本文將給你們介紹ECMAScript中的通常對象之一——函數。咱們將着重介紹不一樣類型的函數以及不一樣類型的函數是如何影響上下文的變量對象以及函數的做用域鏈的。 咱們還會解釋常常會問到的問題,諸如:「不一樣方式建立出來的函數會不同嗎?(若是會,那麼到底有什麼不同呢?)」:html
var foo = function () {
...
};
複製代碼
上述方式建立的函數和以下方式建立的有什麼不一樣?算法
function foo() {
...
}
複製代碼
以下代碼中,爲啥一個函數要用括號包起來呢?express
(function () {
...
})();
複製代碼
因爲本文和此前幾篇文章都是有關聯的,所以,要想徹底搞懂這部份內容,建議先去閱讀第二章-變量對象 以及第四章-做用域鏈。數組
下面,來咱們先來介紹下函數類型。bash
function exampleFunc() {
...
}
複製代碼
這類函數的主要特性是:只有它們能夠影響變量對象(存儲在上下文的VO中)。此特性同時也引出了很是重要的一點(變量對象的天生特性致使的) —— 它們在執行代碼階段就已經存在了(由於FD在進入上下文階段就收集到了VO中)。ecmascript
下面是例子(從代碼位置上來看,函數調用在聲明以前 也就是函數提高):ide
foo();
function foo() {
alert('foo');
}
複製代碼
從定義中還提到了很是重要的一點 —— 函數聲明在代碼中的位置:函數
// 函數聲明能夠直接在程序級別的全局上下文中
function globalFD() {
// 或者直接在另一個函數的函數體中
function innerFD() {}
}
複製代碼
除了上述提到了兩個位置,其餘位置均不能出現函數聲明 —— 比方說,在表達式的位置或者是代碼塊中進行函數聲明都是不能夠的。post
介紹完了函數聲明,接下來介紹函數表達式(function expression)。 測試
這類函數的主要特性是:它們的代碼老是在表達式的位置。最簡單的表達式的例子就是賦值表達式:
var foo = function () {
...
};
複製代碼
上述例子中將一個匿名FE賦值給了變量「foo」,以後該函數就能夠經過「foo」來訪問了—— foo()。
正如定義中提到的,FE也能夠有名字:
var foo = function _foo() {
...
};
複製代碼
這裏要注意的是,在FE的外部能夠經過變量「foo」——foo()來訪問,而在函數內部(好比遞歸調用),還能夠用「_foo」(譯者注:但在外部是沒法使用「_foo」的)。
當FE有名字的時候,它很難和FD做區分。不過,若是仔細看這二者的定義的話,要區分它們仍是很容易的: FE老是在表達式的位置。 以下例子展現的各種ECMAScript表達式都屬於FE:
// 在括號中(grouping operator)只多是表達式
(function foo() {});
// 在數組初始化中 —— 一樣也只能是表達式
[function bar() {}];
// 逗號操做符也只能跟表達式
1, function baz() {};
複製代碼
定義中還提到FE是在執行代碼階段建立的,而且不是存儲在變量對象上的。以下所示:
// 不管是在定義前仍是定義後,FE都是沒法訪問的
// (由於它是在代碼執行階段建立出來的),
alert(foo); // "foo" is not defined
(function foo() {});
// 後面也沒用,由於它根本就不在VO中
alert(foo); // "foo" is not defined
複製代碼
問題來了,FE要來幹嗎?其實答案是很明顯的 —— 在表達式中使用,從而避免對變量對象形成「污染」。最簡單的例子就是將函數做爲參數傳遞給另一個函數:
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function baz() {
alert('foo.baz');
});
複製代碼
上述例子中,部分變量存儲了對FE的引用,這樣函數就會保留在內存中並在以後,能夠經過變量來訪問(由於變量是能夠影響VO的):
var foo = function () {
alert('foo');
};
foo();
複製代碼
以下例子是經過建立一個封裝的做用域來對外部上下文隱藏輔助數據(例子中咱們使用FE使得函數建立後就立馬執行):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
alert(x);
};
})();
foo.bar(); // 10;
alert(x); // "x" is not defined
複製代碼
咱們看到函數「foo.bar」(經過其[[Scope]]屬性)得到了對函數「initialize」內部變量「x」的訪問。 而一樣的「x」在外部就沒法訪問到。不少庫都使用這種策略來建立「私有」數據以及隱藏輔助數據。一般,這樣的狀況下FE的名字都會省略掉:
(function () {
// 初始化做用域
})();
複製代碼
還有一個FE的例子是:在執行代碼階段在條件語句中建立FE,這種方式也不會影響VO:
var foo = 10;
var bar = (foo % 2 == 0
? function () { alert(0); }
: function () { alert(1); }
);
bar(); // 0
複製代碼
標準中提到,表達式語句(ExpressionStatement)不能以左大括號{開始 —— 由於這樣一來就和代碼塊衝突了, 也不能以function關鍵字開始,由於這樣一來又和函數聲明衝突了。比方說,以以下所示的方式來定義一個立馬要執行的函數:
function () {
...
}();
// or with a name
function foo() {
...
}();
複製代碼
對於這兩種狀況,解釋器都會拋出錯誤,只是緣由不一樣。
若是咱們是在全局代碼(程序級別)中這樣定義函數,解釋器會以函數聲明來處理,由於它看到了是以function開始的。 在第一個例子中,會拋出語法錯誤,緣由是既然是個函數聲明,則缺乏函數名了(一個函數聲明其名字是必須的)。
而在第二個例子中,看上去已經有了名字了(foo),應該會正確執行。然而,這裏仍是會拋出語法錯誤 —— 組操做符內部缺乏表達式。 這裏要注意的是,這個例子中,函數聲明後面的()會被當組操做符來處理,而非函數調用的()。所以,若是咱們有以下代碼:
// "foo" 是函數聲明
// 而且是在進入上下文的時候建立的
alert(foo); // function
function foo(x) {
alert(x);
}(1); // 這裏只是組操做符,並不是調用!
foo(10); // 這裏就是調用了, 10
複製代碼
上述代碼其實就是以下代碼:
// function declaration
function foo(x) {
alert(x);
}
// 含表達式的組操做符
(1);
// 另一個組操做符
// 包含一個函數表達式
(function () {});
// 這裏面也是表達式
("foo");
// etc
複製代碼
當這樣的定義出如今語句位置時,也會發生衝突併產生語法錯誤:
if (true) function foo() {alert(1)}
複製代碼
上述結構根據標準規定是不合法的。(表達式是不能以function關鍵字開始的),然而,正如咱們在後面要看到的,沒有一種實現對其拋出錯誤, 它們各自按照本身的方式在處理。
講了這麼多,那究竟要怎麼寫才能達到建立一個函數後立馬就進行調用的目的呢? 答案是很明顯的。它必需要是個函數表達式,而不能是函數聲明。而建立表達式最簡單的方式就是使用上述提到的組操做符。由於在組操做符中只多是表達式。 這樣一來解釋器也不會糾結了,會果斷將其以FE的方式來處理。這樣的函數將在執行階段建立出來,而後立馬執行,隨後被移除(若是有沒有對其的引用的話):
(function foo(x) {
alert(x);
})(1); // 好了,這樣就是函數調用了,而再也不是組操做符了,1
複製代碼
要注意的是,在下面的例子中,函數調用,其括號就再也不是必須的了,由於函數原本就在表達式的位置了,解釋器天然會以FE來處理,而且會在執行代碼階段建立該函數:
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
複製代碼
所以,對「括號有關」問題的完整的回答則以下所示:
若是要在函數建立後立馬進行函數調用,而且函數不在表達式的位置時,
括號就是必須的 —— 這樣狀況下,實際上是手動的將其轉換成了FE。
而當解釋器直接將其以FE的方式處理的時候,說明FE自己就在函數表達式的位置 —— 這個時候括號就不是必須的了。
複製代碼
另外,除了使用括號的方式將函數轉換成爲FE以外,還有其餘的方式,以下所示:
1, function () {
alert('anonymous function is called');
}();
// 或者這樣
!function () {
alert('ECMAScript');
}();
// 固然,還有其餘不少方式
...
複製代碼
不過,括號是最通用也是最優雅的方式。
順便提下,組操做符既能夠包含沒有調用括號的函數,又能夠包含有調用括號的函數,這二者都是合法的FE:
(function () {})();
(function () {}());
複製代碼
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 仍是 0 ? 在不一樣引擎中測試
複製代碼
這裏有必要提下:根據標準,上述代碼結構是不合法的,由於,此前咱們就介紹過,函數聲明是不能出如今代碼塊中的(這裏if和else就包含代碼塊)。 此前提到的,函數聲明只能出如今兩個位置: 程序級別或者另一個函數的函數體中。 爲何這種結構是錯誤的呢?由於在代碼塊中只容許語句。函數要想在這個位置出現的惟一可能就是要成爲表達式語句。 可是,根據定義表達式語句又不能以左大括號開始(這樣會與代碼塊衝突)也不能以function關鍵字開始(這樣又會和FD衝突)。
然而,在錯誤處理部分,標準容許實現對程序語法進行擴展。而上述例子就是其中一種擴展。目前,全部的實現中都不會對上述狀況拋出錯誤,都會以各自的方式進行處理。
所以根據標準,上述if-else中應當須要FE。然而,絕大多數實現中都在進入上下文的時候在這裏簡單地建立了FD,而且使用了最後一次的聲明。 最後「foo」函數顯示了1,儘管理論上else中的代碼根本不會被執行到。
而SpiderMonkey(TraceMonkey也是)實現中,會將上述狀況以兩種方式來處理: 一方面它不會將這樣的函數以函數聲明來處理(也就意味着函數會在執行代碼階段纔會建立出來), 然而,另一方面,它們又不屬於真正的函數表達式,由於在沒有括號的狀況是不能做函數調用的(一樣會有解析錯誤——和FD衝突),它們仍是存儲在變量對象中。
我認爲SpiderMonkey單獨引入了本身的中間函數類型——(FE+FD),這樣的作法是正確的。這樣的函數會根據時間和對應的條件正確建立出來,不像FE。 和FD有點相似,能夠在外部對其進行訪問。SpiderMonkey將這種語法擴展命名爲函數語句(Function Statement)(簡稱FS);這部分理論在MDN中有具體的介紹。 JavaScript的發明者 Brendan Eich也提到過這類函數類型。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" name is available
})();
// but from the outside, correctly, is not
foo(); // "foo" is not defined
複製代碼
這裏「foo」這個名字究竟保存在哪裏呢?在foo的活躍對象中嗎?非也,由於在foo函數中根本就沒有定義任何「foo」。 那麼是在上層上下文的變量對象中嗎?也不是,由於根據定義——FE是不會影響VO的——正如咱們在外層對其調用的結果所看到的那樣。 那麼,它究竟保存在哪裏了呢?
不賣關子了,立刻來揭曉。當解釋器在執行代碼階段看到了有名字的FE以後,它會在建立FE以前,建立一個輔助型的特殊對象,並把它添加到當前的做用域鏈中。 而後,再建立FE,在這個時候(根據第四章-做用域鏈描述的),函數擁有了[[Scope]]屬性 —— 建立函數所在上下文的做用域鏈(這個時候,在[[Scope]]就有了那個特殊對象)。 以後,特殊對象中惟一的屬性 —— FE的名字添加到了該對象中;其值就是對FE的引用。在最後,當前上下文退出的時候,就會把該特殊對象移除。 用僞代碼來描述此算法就以下所示:
specialObject = {};
Scope = specialObject + Scope;
foo = FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // 從做用域鏈的最前面移除specialObject
複製代碼
這就是爲何在函數外是沒法經過名字訪問到該函數的(由於它並不在上層做用域中存在),而在函數內部卻能夠訪問到。
而這裏要注意的一點是: 在某些實現中,好比Rhino,FE的名字並非保存在特殊對象中的,而是保存在FE的活躍對象中。 再好比微軟的實現 —— JScript,則徹底破壞了FE的規則,直接將該名字保存在上層做用域的變量對象中了,這樣在外部也能夠訪問到。
當在Object.prototype對象上定義一個屬性,並將該屬性值指向一個「根本不存在」的變量時,就可以體現該特性。 好比,以下例子中的變量「x」,在查詢過程當中,經過做用域鏈,一直到全局對象也是找不到「x」的。 然而,在SpiderMonkey中,全局對象繼承自Object.prototype,因而,對應的值就在該對象中找到了:
Object.prototype.x = 10;
(function () {
alert(x); // 10
})();
複製代碼
活躍對象是沒有原型一說的。能夠經過內部函數還證實。 若是在定義一個局部變量「x」並聲明一個內部函數(FD或者匿名的FE),而後,在內部函數中引用變量「x」,這個時候該變量會在上層函數上下文中查詢到(理應如此),而不是在Object.prototype中:
Object.prototype.x = 10;
function foo() {
var x = 20;
// function declaration
function bar() {
alert(x);
}
bar(); // 20, from AO(foo)
// the same with anonymous FE
(function () {
alert(x); // 20, also from AO(foo)
})();
}
foo();
複製代碼
在有些實現中,存在這樣的異常:它們會在活躍對象設置原型。比方說,在Blackberry的實現中,上述例子中變量「x」值就會變成10。 由於,「x」從Object.prototype中就找到了:
AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
複製代碼
當出現有名字的FE的特殊對象的時候,在SpiderMonkey中也是有一樣的異常。該特殊對象是常見對象 —— 「和經過new Object()表達式產生的同樣」。 相應地,它也應當繼承自Object.prototype,上述描述只針對SpiderMonkey(1.7版本)。其餘的實現(包括新的TraceMonkey)是不會給這個特殊對象設置原型的:
function foo() {
var x = 10;
(function bar() {
alert(x); // 20, but not 10
// "x" is resolved by the chain:
// AO(bar) - no -> __specialObject(bar) -> no
// __specialObject(bar).[[Prototype]] - yes: 20
})();
}
Object.prototype.x = 20;
foo();
複製代碼
第一,針對上述這樣的狀況,JScript徹底破壞了FE的規則:不該當將函數名字保存在變量對象中的。 另外,FE的名字應當保存在特殊對象中,而且只有在函數自身內部才能夠訪問(其餘地方均不能夠)。而JScript卻將其直接保存在上層上下文的變量對象中。 而且,JScript竟然還將FE以FD的方式處理,在進入上下文的時候就將其建立出來,並在定義以前就能夠訪問到:
// FE 保存在變量對象中
// 和FD同樣,在定義前就能夠經過名字訪問到
testNFE();
(function testNFE() {
alert('testNFE');
});
// 一樣的,在定義以後也能夠經過名字訪問到
testNFE();
複製代碼
正如你們所見,徹底破壞了FE的規則。
第二,在聲明同時,將NFE賦值給一個變量的時候,JScript會建立兩個不一樣的函數對象。 這種行爲感受徹底不符合邏輯(特別是考慮到在NFE外層,其名字根本是沒法訪問到的):
var foo = function bar() {
alert('foo');
};
alert(typeof bar); // "function", NFE 有在VO中了 – 這裏就錯了
// 而後,還有更有趣的
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // undefined
// 然而,兩個函數徹底作的是一樣的事情
foo(); // "foo"
bar(); // "foo"
複製代碼
然而,要注意的是: 當將NFE和賦值給變量這兩件事情分開的話(好比,經過組操做符),在定義好後,再進行變量賦值,這樣,兩個對象就相同了,返回true:
(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10
複製代碼
這個時候就好解釋了。實施上,一開始的確建立了兩個對象,不過以後就只剩下一個了。這裏將NFE以FD的方式來處理,而後,當進入上下文的時候,FD bar就建立出來了。 在這以後,到了執行代碼階段,又建立出了第二個對象 —— FE bar,該對象不會進行保存。相應的,因爲沒有變量對其進行引用,隨後FE bar對象就被移除了。 所以,這裏就只剩下一個對象——FD bar對象,對該對象的引用就賦值給了foo變量。
第三,經過arguments.callee對一個函數進行間接引用,它引用的是和激活函數名一致的對象(事實上是——函數,由於有兩個對象):
var foo = function bar() {
alert([
arguments.callee === foo,
arguments.callee === bar
]);
};
foo(); // [true, false]
bar(); // [false, true]
複製代碼
第四,JScript會將NFE以FD來處理,但當遇到條件語句又不遵循此規則了。好比說,和FD那樣,NFE會在進入上下文的時候就建立出來,這樣最後一次定義的就會被使用:
var foo = function bar() {
alert(1);
};
if (false) {
foo = function bar() {
alert(2);
};
}
bar(); // 2
foo(); // 1
複製代碼
上述行爲從邏輯上也是能夠解釋通的: 當進入上下文的時候,最後一次定義的FD bar被建立出來(有alert(2)的函數), 以後到了執行代碼階段又一個新的函數 —— FE bar被建立出來,對其引用賦值給了變量foo。所以(if代碼塊中因爲判斷條件是false,所以其代碼塊中的代碼永遠不會被執行到)foo函數的調用會打印出1。 儘管「邏輯上」是對的,可是這個仍然算是IE的bug。由於它明顯就破壞了實現的規則,因此我這裏用了引號「邏輯上」。
第五個JScript中NFE的bug和經過給一個未受限的標識符賦值(也就是說,沒有var關鍵字)來建立全局對象的屬性相關。 因爲這裏NFE會以FD的方式來處理,並相應地會保存在變量對象上,賦值給未受限的標識符(不是給變量而是給全局對象的通常屬性), 當函數名和標識符名字相同的時候,該屬性就不會是全局的了。
(function () {
// 沒有var,就不是局部變量,而是全局對象的屬性
foo = function foo() {};
})();
// 然而,在匿名函數的外層,foo又是不可訪問的
alert(typeof foo); // undefined
複製代碼
這裏從「邏輯上」又是能夠解釋通的: 進入上下文時,函數聲明在匿名函數本地上下文的活躍對象中。 當進入執行代碼階段的時候,由於foo這個名字已經在AO中存在了(本地),相應地,賦值操做也只是簡單的對AO中的foo進行更新而已。 並無在全局對象上建立新的屬性。
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" is not defined
}
複製代碼
咱們看到bar函數的[[Scope]]屬性並未包含foo上下文的AO —— 變量「y」是沒法訪問的,而且變量「x」是來自全局上下文。 順便提下,這裏要注意的是,Function構造器能夠經過new關鍵字和省略new關鍵字兩種用法。上述例子中,這兩種用法都是同樣的。
此類函數其餘特性則和同類語法產生式以及聯合對象有關。 該機制在標準中建議在做優化的時候採用(固然,具體的實現者也徹底有權利不使用這類優化)。比方說,有100元素的數組,在循環數組過程當中會給數組每一個元素賦值(函數), 這個時候,實現的時候就能夠採用聯合對象的機制了。這樣,最終全部的數組元素都會引用同一個函數(只有一個函數)
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = function () {}; // 這裏就可使用聯合對象
}
複製代碼
可是,經過Function構造器建立的函數就沒法使用聯合對象了:
var a = [];
for (var k = 0; k $lt; 100; k++) {
a[k] = Function(''); // 只能是100個不一樣的函數
}
複製代碼
下面是另一個和聯合對象相關的例子:
function foo() {
function bar(z) {
return z * z;
}
return bar;
}
var x = foo();
var y = foo();
複製代碼
上述例子,在實現過程當中一樣可使用聯合對象。來使得x和y引用同一個對象,由於函數(包括它們內部的[[Scope]]屬性)物理上是不可分辨的。 所以,經過Function構造器建立的函數老是會佔用更多內存資源。
以下所示使用僞代碼表示的函數建立的算法(不包含聯合對象的步驟)。有助於理解ECMAScript中的函數對象。此算法對全部函數類型都是同樣的。
F = new NativeObject();
// 屬性 [[Class]] is "Function"
F.[[Class]] = "Function"
// 函數對象的原型
F.[[Prototype]] = Function.prototype
// 對函數自身引用
// [[Call]] 在函數調用時F()激活
// 同時建立一個新的執行上下文
F.[[Call]] = <reference to function>
// 內置的構造器
// [[Construct]] 會在使用「new」關鍵字的時候激活
// 事實上,它會爲新對象申請內存
// 而後調用 F.[[Call]]來初始化建立的對象,將this值設置爲新建立的對象
F.[[Construct]] = internalConstructor
// 當前上下文(建立函數F的上下文)的做用域名鏈
F.[[Scope]] = activeContext.Scope
// 若是是經過new Function(...)來建立的,則
F.[[Scope]] = globalContext.Scope
// 形參的個數
F.length = countParameters
// 經過F建立出來的對象的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, 在遍歷中不能枚舉
F.prototype = __objectPrototype
return F
複製代碼
要注意的是,F.[[Prototype]]是函數(構造器)的原型,而F.prototype是經過該函數建立出來的對象的原型(由於一般對這兩個概念都會混淆,在有些文章中會將F.prototype叫作「構造器的原型」,這是錯誤的)。
重學JavaScript深刻理解系列(一)
重學JavaScript深刻理解系列(二)
重學JavaScript深刻理解系列(三)
重學JavaScript深刻理解系列(四)
重學JavaScript深刻理解系列(六)