<header id="function.intro">
# 函數
</header>
## 函數聲明與表達式
函數是JavaScript中的一等對象,這意味著可以把函數像其它值一樣傳遞。 一個常見的用法是把_匿名函數_作為回調函數傳遞到異步函數中。
### 函數聲明
```
function foo() {}
```
上面的方法會在執行前被 [解析(hoisted)](#function.scopes),因此它存在于當前上下文的_任意_一個地方, 即使在函數定義體的上面被調用也是對的。
```
foo(); // 正常運行,因為foo在代碼運行前已經被創建
function foo() {}
```
### 函數賦值表達式
```
var foo = function() {};
```
這個例子把一個_匿名_的函數賦值給變量 `foo`。
```
foo; // 'undefined'
foo(); // 出錯:TypeError
var foo = function() {};
```
由于 `var` 定義了一個聲明語句,對變量 `foo` 的解析是在代碼運行之前,因此 `foo` 變量在代碼運行時已經被定義過了。
但是由于賦值語句只在運行時執行,因此在相應代碼執行之前, `foo` 的值缺省為 [undefined](#core.undefined)。
### 命名函數的賦值表達式
另外一個特殊的情況是將命名函數賦值給一個變量。
```
var foo = function bar() {
bar(); // 正常運行
}
bar(); // 出錯:ReferenceError
```
`bar` 函數聲明外是不可見的,這是因為我們已經把函數賦值給了 `foo`; 然而在 `bar` 內部依然可見。這是由于 JavaScript 的 [命名處理](#function.scopes) 所致, 函數名在函數內_總是_可見的。
**注意:**在IE8及IE8以下版本瀏覽器bar在外部也是可見的,是因為瀏覽器對命名函數賦值表達式進行了錯誤的解析, 解析成兩個函數 `foo` 和 `bar`
## `this` 的工作原理
JavaScript 有一套完全不同于其它語言的對 `this` 的處理機制。 在**五**種不同的情況下 ,`this` 指向的各不相同。
### 全局范圍內
```
this;
```
當在全部范圍內使用 `this`,它將會指向_全局_對象。
**[譯者注](http://cnblogs.com/sanshi/):**瀏覽器中運行的 JavaScript 腳本,這個全局對象是 `window`。
### 函數調用
```
foo();
```
這里 `this` 也會指向_全局_對象。
**ES5 注意:** 在嚴格模式下(strict mode),不存在全局變量。 這種情況下 `this` 將會是 `undefined`。
### 方法調用
```
test.foo();
```
這個例子中,`this` 指向 `test` 對象。
### 調用構造函數
```
new foo();
```
如果函數傾向于和 `new` 關鍵詞一塊使用,則我們稱這個函數是 [構造函數](#function.constructors)。 在函數內部,`this` 指向_新創建_的對象。
### 顯式的設置 `this`
```
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // 數組將會被擴展,如下所示
foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3
```
當使用 `Function.prototype` 上的 `call` 或者 `apply` 方法時,函數內的 `this` 將會被 **顯式設置**為函數調用的第一個參數。
因此_函數調用_的規則在上例中已經不適用了,在`foo` 函數內 `this` 被設置成了 `bar`。
**注意:** 在對象的字面聲明語法中,`this` **不能**用來指向對象本身。 因此 `var obj = {me: this}` 中的 `me` 不會指向 `obj`,因為 `this` 只可能出現在上述的五種情況中。 **[譯者注](http://cnblogs.com/sanshi/):**這個例子中,如果是在瀏覽器中運行,`obj.me` 等于 `window` 對象。
### 常見誤解
盡管大部分的情況都說的過去,不過第一個規則(**[譯者注](http://cnblogs.com/sanshi/):**這里指的應該是第二個規則,也就是直接調用函數時,`this` 指向全局對象) 被認為是JavaScript語言另一個錯誤設計的地方,因為它**從來**就沒有實際的用途。
```
Foo.method = function() {
function test() {
// this 將會被設置為全局對象(譯者注:瀏覽器環境中也就是 window 對象)
}
test();
}
```
一個常見的誤解是 `test` 中的 `this` 將會指向 `Foo` 對象,實際上**不是**這樣子的。
為了在 `test` 中獲取對 `Foo` 對象的引用,我們需要在 `method` 函數內部創建一個局部變量指向 `Foo` 對象。
```
Foo.method = function() {
var that = this;
function test() {
// 使用 that 來指向 Foo 對象
}
test();
}
```
`that` 只是我們隨意起的名字,不過這個名字被廣泛的用來指向外部的 `this` 對象。 在 [閉包](#function.closures) 一節,我們可以看到 `that` 可以作為參數傳遞。
### 方法的賦值表達式
另一個看起來奇怪的地方是函數別名,也就是將一個方法**賦值**給一個變量。
```
var test = someObject.methodTest;
test();
```
上例中,`test` 就像一個普通的函數被調用;因此,函數內的 `this` 將不再被指向到 `someObject` 對象。
雖然 `this` 的晚綁定特性似乎并不友好,但這確實是[基于原型繼承](#object.prototype)賴以生存的土壤。
```
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
```
當 `method` 被調用時,`this` 將會指向 `Bar` 的實例對象。
## 閉包和引用
閉包是 JavaScript 一個非常重要的特性,這意味著當前作用域**總是**能夠訪問外部作用域中的變量。 因為 [函數](#function.scopes) 是 JavaScript 中唯一擁有自身作用域的結構,因此閉包的創建依賴于函數。
### 模擬私有變量
```
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
```
這里,`Counter` 函數返回兩個閉包,函數 `increment` 和函數 `get`。 這兩個函數都維持著 對外部作用域 `Counter` 的引用,因此總可以訪問此作用域內定義的變量 `count`.
### 為什么不可以在外部訪問私有變量
因為 JavaScript 中不可以對作用域進行引用或賦值,因此沒有辦法在外部訪問 `count` 變量。 唯一的途徑就是通過那兩個閉包。
```
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
```
上面的代碼**不會**改變定義在 `Counter` 作用域中的 `count` 變量的值,因為 `foo.hack` 沒有 定義在那個**作用域**內。它將會創建或者覆蓋_全局_變量 `count`。
### 循環中的閉包
一個常見的錯誤出現在循環中使用閉包,假設我們需要在每次循環中調用循環序號
```
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
```
上面的代碼不會輸出數字 `0` 到 `9`,而是會輸出數字 `10` 十次。
當 `console.log` 被調用的時候,_匿名_函數保持對外部變量 `i` 的引用,此時 `for`循環已經結束, `i` 的值被修改成了 `10`.
為了得到想要的結果,需要在每次循環中創建變量 `i` 的**拷貝**。
### 避免引用錯誤
為了正確的獲得循環序號,最好使用 [匿名包裝器](#function.scopes)(**[譯者注](http://cnblogs.com/sanshi/):**其實就是我們通常說的自執行匿名函數)。
```
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
```
外部的匿名函數會立即執行,并把 `i` 作為它的參數,此時函數內 `e` 變量就擁有了 `i` 的一個拷貝。
當傳遞給 `setTimeout` 的匿名函數執行時,它就擁有了對 `e` 的引用,而這個值是**不會**被循環改變的。
有另一個方法完成同樣的工作,那就是從匿名包裝器中返回一個函數。這和上面的代碼效果一樣。
```
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
```
## `arguments` 對象
JavaScript 中每個函數內都能訪問一個特別變量 `arguments`。這個變量維護著所有傳遞到這個函數中的參數列表。
**注意:** 由于 `arguments` 已經被定義為函數內的一個變量。 因此通過 `var` 關鍵字定義 `arguments` 或者將 `arguments` 聲明為一個形式參數, 都將導致原生的 `arguments` 不會被創建。
`arguments` 變量**不是**一個數組(`Array`)。 盡管在語法上它有數組相關的屬性 `length`,但它不從 `Array.prototype` 繼承,實際上它是一個對象(`Object`)。
因此,無法對 `arguments` 變量使用標準的數組方法,比如 `push`, `pop` 或者 `slice`。 雖然使用 `for` 循環遍歷也是可以的,但是為了更好的使用數組方法,最好把它轉化為一個真正的數組。
### 轉化為數組
下面的代碼將會創建一個新的數組,包含所有 `arguments` 對象中的元素。
```
Array.prototype.slice.call(arguments);
```
這個轉化比較**慢**,在性能不好的代碼中**不推薦**這種做法。
### 傳遞參數
下面是將參數從一個函數傳遞到另一個函數的推薦做法。
```
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// 干活
}
```
另一個技巧是同時使用 `call` 和 `apply`,創建一個快速的解綁定包裝器。
```
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// 創建一個解綁定的 "method"
// 輸入參數為: this, arg1, arg2...argN
Foo.method = function() {
// 結果: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
```
**[譯者注](http://cnblogs.com/sanshi/)**:上面的 `Foo.method` 函數和下面代碼的效果是一樣的:
```
Foo.method = function() {
var args = Array.prototype.slice.call(arguments);
Foo.prototype.method.apply(args[0], args.slice(1));
};
```
### 自動更新
`arguments` 對象為其內部屬性以及函數形式參數創建 _getter_ 和 _setter_ 方法。
因此,改變形參的值會影響到 `arguments` 對象的值,反之亦然。
```
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
```
### 性能真相
不管它是否有被使用,`arguments` 對象總會被創建,除了兩個特殊情況 - 作為局部變量聲明和作為形式參數。
`arguments` 的 _getters_ 和 _setters_ 方法總會被創建;因此使用 `arguments` 對性能不會有什么影響。 除非是需要對 `arguments` 對象的屬性進行多次訪問。
**ES5 提示:** 這些 _getters_ 和 _setters_ 在嚴格模式下(strict mode)不會被創建。
**[譯者注](http://cnblogs.com/sanshi/):**在 [MDC](https://developer.mozilla.org/en/JavaScript/Strict_mode) 中對 `strict mode` 模式下 `arguments` 的描述有助于我們的理解,請看下面代碼:
```
// 闡述在 ES5 的嚴格模式下 `arguments` 的特性
function f(a) {
"use strict";
a = 42;
return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
```
然而,的確有一種情況會顯著的影響現代 JavaScript 引擎的性能。這就是使用 `arguments.callee`。
```
function foo() {
arguments.callee; // do something with this function object
arguments.callee.caller; // and the calling function object
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}
```
上面代碼中,`foo` 不再是一個單純的內聯函數 [inlining](http://en.wikipedia.org/wiki/Inlining)(**[譯者注](http://cnblogs.com/sanshi/)**:這里指的是解析器可以做內聯處理), 因為它需要知道它自己和它的調用者。 這不僅抵消了內聯函數帶來的性能提升,而且破壞了封裝,因此現在函數可能要依賴于特定的上下文。
因此**強烈**建議大家**不要**使用 `arguments.callee` 和它的屬性。
**ES5 提示:** 在嚴格模式下,`arguments.callee` 會報錯 `TypeError`,因為它已經被廢除了。
## 構造函數
JavaScript 中的構造函數和其它語言中的構造函數是不同的。 通過 `new` 關鍵字方式調用的函數都被認為是構造函數。
在構造函數內部 - 也就是被調用的函數內 - `this` 指向新創建的對象 `Object`。 這個**新創建**的對象的 [`prototype`](#object.prototype) 被指向到構造函數的 `prototype`。
如果被調用的函數沒有顯式的 `return` 表達式,則隱式的會返回 `this` 對象 - 也就是新創建的對象。
```
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
```
上面代碼把 `Foo` 作為構造函數調用,并設置新創建對象的 `prototype` 為 `Foo.prototype`。
顯式的 `return` 表達式將會影響返回結果,但**僅限**于返回的是一個對象。
```
function Bar() {
return 2;
}
new Bar(); // 返回新創建的對象
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // 返回的對象
```
**[譯者注](http://cnblogs.com/sanshi/):**`new Bar()` 返回的是新創建的對象,而不是數字的字面值 2。 因此 `new Bar().constructor === Bar`,但是如果返回的是數字對象,結果就不同了,如下所示
```
function Bar() {
return new Number(2);
}
new Bar().constructor === Number
```
**[譯者注](http://cnblogs.com/sanshi/):**這里得到的 `new Test()`是函數返回的對象,而不是通過`new`關鍵字新創建的對象,因此:
```
(new Test()).value === undefined
(new Test()).foo === 1
```
如果 `new` 被遺漏了,則函數**不會**返回新創建的對象。
```
function Foo() {
this.bla = 1; // 獲取設置全局參數
}
Foo(); // undefined
```
雖然上例在有些情況下也能正常運行,但是由于 JavaScript 中 [`this`](#function.this) 的工作原理, 這里的 `this` 指向_全局對象_。
### 工廠模式
為了不使用 `new` 關鍵字,構造函數必須顯式的返回一個值。
```
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
```
上面兩種對 `Bar` 函數的調用返回的值完全相同,一個新創建的擁有 `method` 屬性的對象被返回, 其實這里創建了一個[閉包](#function.closures)。
還需要注意, `new Bar()` 并**不會**改變返回對象的原型(**[譯者注](http://cnblogs.com/sanshi/):**也就是返回對象的原型不會指向 `Bar.prototype`)。 因為構造函數的原型會被指向到剛剛創建的新對象,而這里的 `Bar` 沒有把這個新對象返回([譯者注](http://cnblogs.com/sanshi/):而是返回了一個包含 `method` 屬性的自定義對象)。
在上面的例子中,使用或者不使用 `new` 關鍵字沒有功能性的區別。
**[譯者注](http://cnblogs.com/sanshi/):**上面兩種方式創建的對象不能訪問 `Bar` 原型鏈上的屬性,如下所示:
```
var bar1 = new Bar();
typeof(bar1.method); // "function"
typeof(bar1.foo); // "undefined"
var bar2 = Bar();
typeof(bar2.method); // "function"
typeof(bar2.foo); // "undefined"
```
### 通過工廠模式創建新對象
我們常聽到的一條忠告是**不要**使用 `new` 關鍵字來調用函數,因為如果忘記使用它就會導致錯誤。
為了創建新對象,我們可以創建一個工廠方法,并且在方法內構造一個新對象。
```
function Foo() {
var obj = {};
obj.value = 'blub';
var private = 2;
obj.someMethod = function(value) {
this.value = value;
}
obj.getPrivate = function() {
return private;
}
return obj;
}
```
雖然上面的方式比起 `new` 的調用方式不容易出錯,并且可以充分利用[私有變量](#function.closures)帶來的便利, 但是隨之而來的是一些不好的地方。
1. 會占用更多的內存,因為新創建的對象**不能**共享原型上的方法。
2. 為了實現繼承,工廠方法需要從另外一個對象拷貝所有屬性,或者把一個對象作為新創建對象的原型。
3. 放棄原型鏈僅僅是因為防止遺漏 `new` 帶來的問題,這似乎和語言本身的思想相違背。
### 總結
雖然遺漏 `new` 關鍵字可能會導致問題,但這并**不是**放棄使用原型鏈的借口。 最終使用哪種方式取決于應用程序的需求,選擇一種代碼書寫風格并**堅持**下去才是最重要的。
## 作用域與命名空間
盡管 JavaScript 支持一對花括號創建的代碼段,但是并不支持塊級作用域; 而僅僅支持 _函數作用域_。
```
function test() { // 一個作用域
for(var i = 0; i < 10; i++) { // 不是一個作用域
// count
}
console.log(i); // 10
}
```
**注意:** 如果不是在賦值語句中,而是在 return 表達式或者函數參數中,`{...}` 將會作為代碼段解析, 而不是作為對象的字面語法解析。如果考慮到 [自動分號插入](#core.semicolon),這可能會導致一些不易察覺的錯誤。
**[譯者注](http://cnblogs.com/sanshi/):**如果 `return` 對象的左括號和 `return` 不在一行上就會出錯。
```
// 譯者注:下面輸出 undefined
function add(a, b) {
return
a + b;
}
console.log(add(1, 2));
```
JavaScript 中沒有顯式的命名空間定義,這就意味著所有對象都定義在一個_全局共享_的命名空間下面。
每次引用一個變量,JavaScript 會向上遍歷整個作用域直到找到這個變量為止。 如果到達全局作用域但是這個變量仍未找到,則會拋出 `ReferenceError` 異常。
### 隱式的全局變量
```
// 腳本 A
foo = '42';
// 腳本 B
var foo = '42'
```
上面兩段腳本效果**不同**。腳本 A 在_全局_作用域內定義了變量 `foo`,而腳本 B 在_當前_作用域內定義變量 `foo`。
再次強調,上面的效果**完全不同**,不使用 `var` 聲明變量將會導致隱式的全局變量產生。
```
// 全局作用域
var foo = 42;
function test() {
// 局部作用域
foo = 21;
}
test();
foo; // 21
```
在函數 `test` 內不使用 `var` 關鍵字聲明 `foo` 變量將會覆蓋外部的同名變量。 起初這看起來并不是大問題,但是當有成千上萬行代碼時,不使用 `var` 聲明變量將會帶來難以跟蹤的 BUG。
```
// 全局作用域
var items = [/* 數組 */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// subLoop 函數作用域
for(i = 0; i < 10; i++) { // 沒有使用 var 聲明變量
// 干活
}
}
```
外部循環在第一次調用 `subLoop` 之后就會終止,因為 `subLoop` 覆蓋了全局變量 `i`。 在第二個 `for` 循環中使用 `var` 聲明變量可以避免這種錯誤。 聲明變量時**絕對不要**遺漏 `var` 關鍵字,除非這就是_期望_的影響外部作用域的行為。
### 局部變量
JavaScript 中局部變量只可能通過兩種方式聲明,一個是作為[函數](#function)參數,另一個是通過 `var` 關鍵字聲明。
```
// 全局變量
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// 函數 test 內的局部作用域
i = 5;
var foo = 3;
bar = 4;
}
test(10);
```
`foo` 和 `i` 是函數 `test` 內的局部變量,而對 `bar` 的賦值將會覆蓋全局作用域內的同名變量。
### 變量聲明提升(Hoisting)
JavaScript 會**提升**變量聲明。這意味著 `var` 表達式和 `function` 聲明都將會被提升到當前作用域的頂部。
```
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
```
上面代碼在運行之前將會被轉化。JavaScript 將會把 `var` 表達式和 `function` 聲明提升到當前作用域的頂部。
```
// var 表達式被移動到這里
var bar, someValue; // 缺省值是 'undefined'
// 函數聲明也會提升
function test(data) {
var goo, i, e; // 沒有塊級作用域,這些變量被移動到函數頂部
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // 出錯:TypeError,因為 bar 依然是 'undefined'
someValue = 42; // 賦值語句不會被提升規則(hoisting)影響
bar = function() {};
test();
```
沒有塊級作用域不僅導致 `var` 表達式被從循環內移到外部,而且使一些 `if` 表達式更難看懂。
在原來代碼中,`if` 表達式看起來修改了_全局變量_ `goo`,實際上在提升規則被應用后,卻是在修改_局部變量_。
如果沒有提升規則(hoisting)的知識,下面的代碼看起來會拋出異常 `ReferenceError`。
```
// 檢查 SomeImportantThing 是否已經被初始化
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
```
實際上,上面的代碼正常運行,因為 `var` 表達式會被提升到_全局作用域_的頂部。
```
var SomeImportantThing;
// 其它一些代碼,可能會初始化 SomeImportantThing,也可能不會
// 檢查是否已經被初始化
if (!SomeImportantThing) {
SomeImportantThing = {};
}
```
**[譯者注](http://cnblogs.com/sanshi/):**在 Nettuts+ 網站有一篇介紹 hoisting 的[文章](http://net.tutsplus.com/tutorials/javascript-ajax/quick-tip-javascript-hoisting-explained/),其中的代碼很有啟發性。
```
// 譯者注:來自 Nettuts+ 的一段代碼,生動的闡述了 JavaScript 中變量聲明提升規則
var myvar = 'my value';
(function() {
alert(myvar); // undefined
var myvar = 'local value';
})(); ?
```
### 名稱解析順序
JavaScript 中的所有作用域,包括_全局作用域_,都有一個特別的名稱 [`this`](#function.this) 指向當前對象。
函數作用域內也有默認的變量 [`arguments`](#function.arguments),其中包含了傳遞到函數中的參數。
比如,當訪問函數內的 `foo` 變量時,JavaScript 會按照下面順序查找:
1. 當前作用域內是否有 `var foo` 的定義。
2. 函數形式參數是否有使用 `foo` 名稱的。
3. 函數自身是否叫做 `foo`。
4. 回溯到上一級作用域,然后從 **#1** 重新開始。
**注意:** 自定義 `arguments` 參數將會阻止原生的 `arguments` 對象的創建。
### 命名空間
只有一個全局作用域導致的常見錯誤是命名沖突。在 JavaScript中,這可以通過 _匿名包裝器_ 輕松解決。
```
(function() {
// 函數創建一個命名空間
window.foo = function() {
// 對外公開的函數,創建了閉包
};
})(); // 立即執行此匿名函數
```
匿名函數被認為是 [表達式](#function);因此為了可調用性,它們首先會被執行。
```
( // 小括號內的函數首先被執行
function() {}
) // 并且返回函數對象
() // 調用上面的執行結果,也就是函數對象
```
有一些其他的調用函數表達式的方法,比如下面的兩種方式語法不同,但是效果一模一樣。
```
// 另外兩種方式
+function(){}();
(function(){}());
```
### 結論
推薦使用_匿名包裝器_(**[譯者注](http://cnblogs.com/sanshi/):**也就是自執行的匿名函數)來創建命名空間。這樣不僅可以防止命名沖突, 而且有利于程序的模塊化。
另外,使用全局變量被認為是**不好的習慣**。這樣的代碼容易產生錯誤并且維護成本較高。