本章節(jié)我們要著重介紹的是一個(gè)非常常見的ECMAScript對(duì)象——函數(shù)(function),我們將詳細(xì)講解一下各種類型的函數(shù)是如何影響上下文的變量對(duì)象以及每個(gè)函數(shù)的作用域鏈都包含什么,以及回答諸如像下面這樣的問題:下面聲明的函數(shù)有什么區(qū)別么?(如果有,區(qū)別是什么)。
原文:http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/
var foo = function () {
...
};
平時(shí)的慣用方式:
function foo() {
...
}
或者,下面的函數(shù)為什么要用括號(hào)括?。?/p>
(function () {
...
})();
關(guān)于具體的介紹,早前面的12章變量對(duì)象和14章作用域鏈都有介紹,如果需要詳細(xì)了解這些內(nèi)容,請(qǐng)查詢上述2個(gè)章節(jié)的詳細(xì)內(nèi)容。
但我們依然要一個(gè)一個(gè)分別看看,首先從函數(shù)的類型講起:
在ECMAScript 中有三種函數(shù)類型:函數(shù)聲明,函數(shù)表達(dá)式和函數(shù)構(gòu)造器創(chuàng)建的函數(shù)。每一種都有自己的特點(diǎn)。
函數(shù)聲明(縮寫為FD)是這樣一種函數(shù):
function exampleFunc() {
...
}
這種函數(shù)類型的主要特點(diǎn)在于它們僅僅影響變量對(duì)象(即存儲(chǔ)在上下文的VO中的變量對(duì)象)。該特點(diǎn)也解釋了第二個(gè)重要點(diǎn)(它是變量對(duì)象特性的結(jié)果)——在代碼執(zhí)行階段它們已經(jīng)可用(因?yàn)镕D在進(jìn)入上下文階段已經(jīng)存在于VO中——代碼執(zhí)行之前)。
例如(函數(shù)在其聲明之前被調(diào)用)
foo();
function foo() {
alert('foo');
}
另外一個(gè)重點(diǎn)知識(shí)點(diǎn)是上述定義中的第二點(diǎn)——函數(shù)聲明在源碼中的位置:
// 函數(shù)可以在如下地方聲明:
// 1) 直接在全局上下文中
function globalFD() {
// 2) 或者在一個(gè)函數(shù)的函數(shù)體內(nèi)
function innerFD() {}
}
只有這2個(gè)位置可以聲明函數(shù),也就是說:不可能在表達(dá)式位置或一個(gè)代碼塊中定義它。
另外一種可以取代函數(shù)聲明的方式是函數(shù)表達(dá)式,解釋如下:
函數(shù)表達(dá)式(縮寫為FE)是這樣一種函數(shù):
這種函數(shù)類型的主要特點(diǎn)在于它在源碼中總是處在表達(dá)式的位置。最簡(jiǎn)單的一個(gè)例子就是一個(gè)賦值聲明:
var foo = function () {
...
};
該例演示是讓一個(gè)匿名函數(shù)表達(dá)式賦值給變量foo,然后該函數(shù)可以用foo這個(gè)名稱進(jìn)行訪問——foo()。
同時(shí)和定義里描述的一樣,函數(shù)表達(dá)式也可以擁有可選的名稱:
var foo = function _foo() {
...
};
需要注意的是,在外部FE通過變量“foo”來訪問——foo(),而在函數(shù)內(nèi)部(如遞歸調(diào)用),有可能使用名稱“_foo”。
如果FE有一個(gè)名稱,就很難與FD區(qū)分。但是,如果你明白定義,區(qū)分起來就簡(jiǎn)單明了:FE總是處在表達(dá)式的位置。在下面的例子中我們可以看到各種ECMAScript 表達(dá)式:
// 圓括號(hào)(分組操作符)內(nèi)只能是表達(dá)式
(function foo() {});
// 在數(shù)組初始化器內(nèi)只能是表達(dá)式
[function bar() {}];
// 逗號(hào)也只能操作表達(dá)式
1, function baz() {};
表達(dá)式定義里說明:FE只能在代碼執(zhí)行階段創(chuàng)建而且不存在于變量對(duì)象中,讓我們來看一個(gè)示例行為:
// FE在定義階段之前不可用(因?yàn)樗窃诖a執(zhí)行階段創(chuàng)建)
alert(foo); // "foo" 未定義
(function foo() {});
// 定義階段之后也不可用,因?yàn)樗辉谧兞繉?duì)象VO中
alert(foo); // "foo" 未定義
相當(dāng)一部分問題出現(xiàn)了,我們?yōu)槭裁葱枰瘮?shù)表達(dá)式?答案很明顯——在表達(dá)式中使用它們,”不會(huì)污染”變量對(duì)象。最簡(jiǎn)單的例子是將一個(gè)函數(shù)作為參數(shù)傳遞給其它函數(shù)。
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function baz() {
alert('foo.baz');
});
在上述例子里,F(xiàn)E賦值給了一個(gè)變量(也就是參數(shù)),函數(shù)將該表達(dá)式保存在內(nèi)存中,并通過變量名來訪問(因?yàn)樽兞坑绊懽兞繉?duì)象),如下:
var foo = function () {
alert('foo');
};
foo();
另外一個(gè)例子是創(chuàng)建封裝的閉包從外部上下文中隱藏輔助性數(shù)據(jù)(在下面的例子中我們使用FE,它在創(chuàng)建后立即調(diào)用):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
alert(x);
};
})();
foo.bar(); // 10;
alert(x); // "x" 未定義
我們看到函數(shù)foo.bar(通過[[Scope]]屬性)訪問到函數(shù)initialize的內(nèi)部變量“x”。同時(shí),“x”在外部不能直接訪問。在許多庫中,這種策略常用來創(chuàng)建”私有”數(shù)據(jù)和隱藏輔助實(shí)體。在這種模式中,初始化的FE的名稱通常被忽略:
(function () {
// 初始化作用域
})();
還有一個(gè)例子是:在代碼執(zhí)行階段通過條件語句進(jìn)行創(chuàng)建FE,不會(huì)污染變量對(duì)象VO。
var foo = 10;
var bar = (foo % 2 == 0
? function () { alert(0); }
: function () { alert(1); }
);
bar(); // 0
關(guān)于圓括號(hào)的問題
讓我們回頭并回答在文章開頭提到的問題——”為何在函數(shù)創(chuàng)建后的立即調(diào)用中必須用圓括號(hào)來包圍它?”,答案就是:表達(dá)式句子的限制就是這樣的。
按照標(biāo)準(zhǔn),表達(dá)式語句不能以一個(gè)大括號(hào){開始是因?yàn)樗茈y與代碼塊區(qū)分,同樣,他也不能以函數(shù)關(guān)鍵字開始,因?yàn)楹茈y與函數(shù)聲明進(jìn)行區(qū)分。即,所以,如果我們定義一個(gè)立即執(zhí)行的函數(shù),在其創(chuàng)建后立即按以下方式調(diào)用:
function () {
...
}();
// 即便有名稱
function foo() {
...
}();
我們使用了函數(shù)聲明,上述2個(gè)定義,解釋器在解釋的時(shí)候都會(huì)報(bào)錯(cuò),但是可能有多種原因。
如果在全局代碼里定義(也就是程序級(jí)別),解釋器會(huì)將它看做是函數(shù)聲明,因?yàn)樗且詅unction關(guān)鍵字開頭,第一個(gè)例子,我們會(huì)得到SyntaxError錯(cuò)誤,是因?yàn)楹瘮?shù)聲明沒有名字(我們前面提到了函數(shù)聲明必須有名字)。
第二個(gè)例子,我們有一個(gè)名稱為foo的一個(gè)函數(shù)聲明正常創(chuàng)建,但是我們依然得到了一個(gè)語法錯(cuò)誤——沒有任何表達(dá)式的分組操作符錯(cuò)誤。在函數(shù)聲明后面他確實(shí)是一個(gè)分組操作符,而不是一個(gè)函數(shù)調(diào)用所使用的圓括號(hào)。所以如果我們聲明如下代碼:
// "foo" 是一個(gè)函數(shù)聲明,在進(jìn)入上下文的時(shí)候創(chuàng)建
alert(foo); // 函數(shù)
function foo(x) {
alert(x);
}(1); // 這只是一個(gè)分組操作符,不是函數(shù)調(diào)用!
foo(10); // 這才是一個(gè)真正的函數(shù)調(diào)用,結(jié)果是10
上述代碼是沒有問題的,因?yàn)槁暶鞯臅r(shí)候產(chǎn)生了2個(gè)對(duì)象:一個(gè)函數(shù)聲明,一個(gè)帶有1的分組操作,上面的例子可以理解為如下代碼:
// 函數(shù)聲明
function foo(x) {
alert(x);
}
// 一個(gè)分組操作符,包含一個(gè)表達(dá)式1
(1);
// 另外一個(gè)操作符,包含一個(gè)function表達(dá)式
(function () {});
// 這個(gè)操作符里,包含的也是一個(gè)表達(dá)式"foo"
("foo");
// 等等
如果我們定義一個(gè)如下代碼(定義里包含一個(gè)語句),我們可能會(huì)說,定義歧義,會(huì)得到報(bào)錯(cuò):
if (true) function foo() {alert(1)}
根據(jù)規(guī)范,上述代碼是錯(cuò)誤的(一個(gè)表達(dá)式語句不能以function關(guān)鍵字開頭),但下面的例子就沒有報(bào)錯(cuò),想想為什么?
我們?nèi)绻麃砀嬖V解釋器:我就像在函數(shù)聲明之后立即調(diào)用,答案是很明確的,你得聲明函數(shù)表達(dá)式function expression,而不是函數(shù)聲明function declaration,并且創(chuàng)建表達(dá)式最簡(jiǎn)單的方式就是用分組操作符括號(hào),里邊放入的永遠(yuǎn)是表達(dá)式,所以解釋器在解釋的時(shí)候就不會(huì)出現(xiàn)歧義。在代碼執(zhí)行階段這個(gè)的function就會(huì)被創(chuàng)建,并且立即執(zhí)行,然后自動(dòng)銷毀(如果沒有引用的話)。
(function foo(x) {
alert(x);
})(1); // 這才是調(diào)用,不是分組操作符
上述代碼就是我們所說的在用括號(hào)括住一個(gè)表達(dá)式,然后通過(1)去調(diào)用。
注意,下面一個(gè)立即執(zhí)行的函數(shù),周圍的括號(hào)不是必須的,因?yàn)楹瘮?shù)已經(jīng)處在表達(dá)式的位置,解析器知道它處理的是在函數(shù)執(zhí)行階段應(yīng)該被創(chuàng)建的FE,這樣在函數(shù)創(chuàng)建后立即調(diào)用了函數(shù)。
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
就像我們看到的,foo.bar是一個(gè)字符串而不是一個(gè)函數(shù),這里的函數(shù)僅僅用來根據(jù)條件參數(shù)初始化這個(gè)屬性——它創(chuàng)建后并立即調(diào)用。
因此,”關(guān)于圓括號(hào)”問題完整的答案如下:當(dāng)函數(shù)不在表達(dá)式的位置的時(shí)候,分組操作符圓括號(hào)是必須的——也就是手工將函數(shù)轉(zhuǎn)化成FE。
如果解析器知道它處理的是FE,就沒必要用圓括號(hào)。
除了大括號(hào)以外,如下形式也可以將函數(shù)轉(zhuǎn)化為FE類型,例如:
// 注意是1,后面的聲明
1, function () {
alert('anonymous function is called');
}();
// 或者這個(gè)
!function () {
alert('ECMAScript');
}();
// 其它手工轉(zhuǎn)化的形式
...
但是,在這個(gè)例子中,圓括號(hào)是最簡(jiǎn)潔的方式。
順便提一句,組表達(dá)式包圍函數(shù)描述可以沒有調(diào)用圓括號(hào),也可包含調(diào)用圓括號(hào),即,下面的兩個(gè)表達(dá)式都是正確的FE。
實(shí)現(xiàn)擴(kuò)展:函數(shù)語句
下面的代碼,根據(jù)貴方任何一個(gè)function聲明都不應(yīng)該被執(zhí)行:
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 or 0 ?實(shí)際在上不同環(huán)境下測(cè)試得出個(gè)結(jié)果不一樣
這里有必要說明的是,按照標(biāo)準(zhǔn),這種句法結(jié)構(gòu)通常是不正確的,因?yàn)槲覀冞€記得,一個(gè)函數(shù)聲明(FD)不能出現(xiàn)在代碼塊中(這里if和else包含代碼塊)。我們?cè)?jīng)講過,F(xiàn)D僅出現(xiàn)在兩個(gè)位置:程序級(jí)(Program level)或直接位于其它函數(shù)體中。
因?yàn)榇a塊僅包含語句,所以這是不正確的??梢猿霈F(xiàn)在塊中的函數(shù)的唯一位置是這些語句中的一個(gè)——上面已經(jīng)討論過的表達(dá)式語句。但是,按照定義它不能以大括號(hào)開始(既然它有別于代碼塊)或以一個(gè)函數(shù)關(guān)鍵字開始(既然它有別于FD)。
但是,在標(biāo)準(zhǔn)的錯(cuò)誤處理章節(jié)中,它允許程序語法的擴(kuò)展執(zhí)行。這樣的擴(kuò)展之一就是我們見到的出現(xiàn)在代碼塊中的函數(shù)。在這個(gè)例子中,現(xiàn)今的所有存在的執(zhí)行都不會(huì)拋出異常,都會(huì)處理它。但是它們都有自己的方式。
if-else分支語句的出現(xiàn)意味著一個(gè)動(dòng)態(tài)的選擇。即,從邏輯上來說,它應(yīng)該是在代碼執(zhí)行階段動(dòng)態(tài)創(chuàng)建的函數(shù)表達(dá)式(FE)。但是,大多數(shù)執(zhí)行在進(jìn)入上下文階段時(shí)簡(jiǎn)單的創(chuàng)建函數(shù)聲明(FD),并使用最后聲明的函數(shù)。即,函數(shù)foo將顯示”1″,事實(shí)上else分支將永遠(yuǎn)不會(huì)執(zhí)行。
但是,SpiderMonkey (和TraceMonkey)以兩種方式對(duì)待這種情況:一方面它不會(huì)將函數(shù)作為聲明處理(即,函數(shù)在代碼執(zhí)行階段根據(jù)條件創(chuàng)建),但另一方面,既然沒有括號(hào)包圍(再次出現(xiàn)解析錯(cuò)誤——”與FD有別”),他們不能被調(diào)用,所以也不是真正的函數(shù)表達(dá)式,它儲(chǔ)存在變量對(duì)象中。
我個(gè)人認(rèn)為這個(gè)例子中SpiderMonkey 的行為是正確的,拆分了它自身的函數(shù)中間類型——(FE+FD)。這些函數(shù)在合適的時(shí)間創(chuàng)建,根據(jù)條件,也不像FE,倒像一個(gè)可以從外部調(diào)用的FD,SpiderMonkey將這種語法擴(kuò)展 稱之為函數(shù)語句(縮寫為FS);該語法在MDC中提及過。
命名函數(shù)表達(dá)式的特性
當(dāng)函數(shù)表達(dá)式FE有一個(gè)名稱(稱為命名函數(shù)表達(dá)式,縮寫為NFE)時(shí),將會(huì)出現(xiàn)一個(gè)重要的特點(diǎn)。從定義(正如我們從上面示例中看到的那樣)中我們知道函數(shù)表達(dá)式不會(huì)影響一個(gè)上下文的變量對(duì)象(那樣意味著既不可能通過名稱在函數(shù)聲明之前調(diào)用它,也不可能在聲明之后調(diào)用它)。但是,F(xiàn)E在遞歸調(diào)用中可以通過名稱調(diào)用自身。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" 是可用的
})();
// 在外部,是不可用的
foo(); // "foo" 未定義
“foo”儲(chǔ)存在什么地方?在foo的活動(dòng)對(duì)象中?不是,因?yàn)樵趂oo中沒有定義任何”foo”。在上下文的父變量對(duì)象中創(chuàng)建foo?也不是,因?yàn)榘凑斩x——FE不會(huì)影響VO(變量對(duì)象)——從外部調(diào)用foo我們可以實(shí)實(shí)在在的看到。那么在哪里呢?
以下是關(guān)鍵點(diǎn)。當(dāng)解釋器在代碼執(zhí)行階段遇到命名的FE時(shí),在FE創(chuàng)建之前,它創(chuàng)建了輔助的特定對(duì)象,并添加到當(dāng)前作用域鏈的最前端。然后它創(chuàng)建了FE,此時(shí)(正如我們?cè)诘谒恼?作用域鏈知道的那樣)函數(shù)獲取了[[Scope]] 屬性——?jiǎng)?chuàng)建這個(gè)函數(shù)上下文的作用域鏈)。此后,F(xiàn)E的名稱添加到特定對(duì)象上作為唯一的屬性;這個(gè)屬性的值是引用到FE上。最后一步是從父作用域鏈中移除那個(gè)特定的對(duì)象。讓我們?cè)趥未a中看看這個(gè)算法:
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // 從作用域鏈中刪除定義的特殊對(duì)象specialObject
因此,在函數(shù)外部這個(gè)名稱不可用的(因?yàn)樗辉诟缸饔糜蜴溨校?,但是,特定?duì)象已經(jīng)存儲(chǔ)在函數(shù)的[[scope]]中,在那里名稱是可用的。
但是需要注意的是一些實(shí)現(xiàn)(如Rhino)不是在特定對(duì)象中而是在FE的激活對(duì)象中存儲(chǔ)這個(gè)可選的名稱。Microsoft 中的執(zhí)行完全打破了FE規(guī)則,它在父變量對(duì)象中保持了這個(gè)名稱,這樣函數(shù)在外部變得可以訪問。
NFE 與SpiderMonkey
我們來看看NFE和SpiderMonkey的區(qū)別,SpiderMonkey 的一些版本有一個(gè)與特定對(duì)象相關(guān)的屬性,它可以作為bug來對(duì)待(雖然按照標(biāo)準(zhǔn)所有的都那樣實(shí)現(xiàn)了,但更像一個(gè)ECMAScript標(biāo)準(zhǔn)上的bug)。它與標(biāo)識(shí)符的解析機(jī)制相關(guān):作用域鏈的分析是二維的,在標(biāo)識(shí)符的解析中,同樣考慮到作用域鏈中每個(gè)對(duì)象的原型鏈。
如果我們?cè)贠bject.prototype中定義一個(gè)屬性,并引用一個(gè)”不存在(nonexistent)”的變量。我們就能看到這種執(zhí)行機(jī)制。這樣,在下面示例的”x”解析中,我們將到達(dá)全局對(duì)象,但是沒發(fā)現(xiàn)”x”。但是,在SpiderMonkey 中全局對(duì)象繼承了Object.prototype中的屬性,相應(yīng)地,”x”也能被解析。
Object.prototype.x = 10;
(function () {
alert(x); // 10
})();
活動(dòng)對(duì)象沒有原型。按照同樣的起始條件,在上面的例子中,不可能看到內(nèi)部函數(shù)的這種行為。如果定義一個(gè)局部變量”x”,并定義內(nèi)部函數(shù)(FD或匿名的FE),然后再內(nèi)部函數(shù)中引用”x”。那么這個(gè)變量將在父函數(shù)上下文(即,應(yīng)該在哪里被解析)中而不是在Object.prototype中被解析。
Object.prototype.x = 10;
function foo() {
var x = 20;
// 函數(shù)聲明
function bar() {
alert(x);
}
bar(); // 20, 從foo的變量對(duì)象AO中查詢
// 匿名函數(shù)表達(dá)式也是一樣
(function () {
alert(x); // 20, 也是從foo的變量對(duì)象AO中查詢
})();
}
foo();
盡管如此,一些執(zhí)行會(huì)出現(xiàn)例外,它給活動(dòng)對(duì)象設(shè)置了一個(gè)原型。因此,在Blackberry 的執(zhí)行中,上面例子中的”x”被解析為”10″。也就是說,既然在Object.prototype中已經(jīng)找到了foo的值,那么它就不會(huì)到達(dá)foo的活動(dòng)對(duì)象。
AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
在SpiderMonkey 中,同樣的情形我們完全可以在命名FE的特定對(duì)象中看到。這個(gè)特定的對(duì)象(按照標(biāo)準(zhǔn))是普通對(duì)象——”就像表達(dá)式new Object()“,相應(yīng)地,它應(yīng)該從Object.prototype 繼承屬性,這恰恰是我們?cè)赟piderMonkey (1.7以上的版本)看到的執(zhí)行。其余的執(zhí)行(包括新的TraceMonkey)不會(huì)為特定的對(duì)象設(shè)置一個(gè)原型。
function foo() {
var x = 10;
(function bar() {
alert(x); // 20, 不上10,不是從foo的活動(dòng)對(duì)象上得到的
// "x"從鏈上查找:
// AO(bar) - no -> __specialObject(bar) -> no
// __specialObject(bar).[[Prototype]] - yes: 20
})();
}
Object.prototype.x = 20;
foo();
NFE與Jscript
當(dāng)前IE瀏覽器(直到JScript 5.8 — IE8)中內(nèi)置的JScript 執(zhí)行有很多與函數(shù)表達(dá)式(NFE)相關(guān)的bug。所有的這些bug都完全與ECMA-262-3標(biāo)準(zhǔn)矛盾;有些可能會(huì)導(dǎo)致嚴(yán)重的錯(cuò)誤。
首先,這個(gè)例子中JScript 破壞了FE的主要規(guī)則,它不應(yīng)該通過函數(shù)名存儲(chǔ)在變量對(duì)象中??蛇x的FE名稱應(yīng)該存儲(chǔ)在特定的對(duì)象中,并只能在函數(shù)自身(而不是別的地方)中訪問。但I(xiàn)E直接將它存儲(chǔ)在父變量對(duì)象中。此外,命名的FE在JScript 中作為函數(shù)聲明(FD)對(duì)待。即創(chuàng)建于進(jìn)入上下文的階段,在源代碼中的定義之前可以訪問。
// FE 在變量對(duì)象里可見
testNFE();
(function testNFE() {
alert('testNFE');
});
// FE 在定義結(jié)束以后也可見
// 就像函數(shù)聲明一樣
testNFE();
正如我們所見,它完全違背了規(guī)則。
其次,在聲明中將命名FE賦給一個(gè)變量時(shí),JScript 創(chuàng)建了兩個(gè)不同的函數(shù)對(duì)象。邏輯上(特別注意的是在NFE的外部它的名稱根本不應(yīng)該被訪問)很難命名這種行為。
var foo = function bar() {
alert('foo');
};
alert(typeof bar); // "function",
// 有趣的是
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // 未定義
// 但執(zhí)行的時(shí)候結(jié)果一樣
foo(); // "foo"
bar(); // "foo"
再次看到,已經(jīng)亂成一片了。
但是,需要注意的是,如果與變量賦值分開,單獨(dú)描述NFE(如通過組運(yùn)算符),然后將它賦給一個(gè)變量,并檢查其相等性,結(jié)果為true,就好像是一個(gè)對(duì)象。
(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10
此時(shí)是可以解釋的。實(shí)際上,再次創(chuàng)建兩個(gè)對(duì)象,但那樣做事實(shí)上仍保持一個(gè)。如果我們?cè)俅握J(rèn)為這里的NFE被作為FD對(duì)待,然后在進(jìn)入上下文階段創(chuàng)建FD bar。此后,在代碼執(zhí)行階段第二個(gè)對(duì)象——函數(shù)表達(dá)式(FE)bar 被創(chuàng)建,它不會(huì)被存儲(chǔ)。相應(yīng)地,沒有FE bar的任何引用,它被移除了。這樣就只有一個(gè)對(duì)象——FD bar,對(duì)它的引用賦給了變量foo。
第三,就通過arguments.callee間接引用一個(gè)函數(shù)而言,它引用的是被激活的那個(gè)對(duì)象的名稱(確切的說——再這里有兩個(gè)函數(shù)對(duì)象。
var foo = function bar() {
alert([
arguments.callee === foo,
arguments.callee === bar
]);
};
foo(); // [true, false]
bar(); // [false, true]
第四,JScript 像對(duì)待普通的FD一樣對(duì)待NFE,他不服從條件表達(dá)式規(guī)則。即,就像一個(gè)FD,NFE在進(jìn)入上下文時(shí)創(chuàng)建,在代碼中最后的定義被使用。
var foo = function bar() {
alert(1);
};
if (false) {
foo = function bar() {
alert(2);
};
}
bar(); // 2
foo(); // 1
這種行為從”邏輯上”也可以解釋。在進(jìn)入上下文階段,最后遇到的FD bar被創(chuàng)建,即包含alert(2)的函數(shù)。此后,在代碼執(zhí)行階段,新的函數(shù)——FE bar創(chuàng)建,對(duì)它的引用賦給了變量foo。這樣foo激活產(chǎn)生alert(1)。邏輯很清楚,但考慮到IE的bug,既然執(zhí)行明顯被破壞,并依賴于JScript 的bug,我給單詞”邏輯上(logically)”加上了引號(hào)。
JScript 的第五個(gè)bug與全局對(duì)象的屬性創(chuàng)建相關(guān),全局對(duì)象由賦值給一個(gè)未限定的標(biāo)識(shí)符(即,沒有var關(guān)鍵字)來生成。既然NFE在這被作為FD對(duì)待,相應(yīng)地,它存儲(chǔ)在變量對(duì)象中,賦給一個(gè)未限定的標(biāo)識(shí)符(即不是賦給變量而是全局對(duì)象的普通屬性),萬一函數(shù)的名稱與未限定的標(biāo)識(shí)符相同,這樣該屬性就不是全局的了。
(function () {
// 不用var的話,就不是當(dāng)前上下文的一個(gè)變量了
// 而是全局對(duì)象的一個(gè)屬性
foo = function foo() {};
})();
// 但,在匿名函數(shù)的外部,foo這個(gè)名字是不可用的
alert(typeof foo); // 未定義
“邏輯”已經(jīng)很清楚了:在進(jìn)入上下文階段,函數(shù)聲明foo取得了匿名函數(shù)局部上下文的活動(dòng)對(duì)象。在代碼執(zhí)行階段,名稱foo在AO中已經(jīng)存在,即,它被作為局部變量。相應(yīng)地,在賦值操作中,只是簡(jiǎn)單的更新已存在于AO中的屬性foo,而不是按照ECMA-262-3的邏輯創(chuàng)建全局對(duì)象的新屬性。
既然這種函數(shù)對(duì)象也有自己的特色,我們將它與FD和FE區(qū)分開來。其主要特點(diǎn)在于這種函數(shù)的[[Scope]]屬性僅包含全局對(duì)象:
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" 未定義
}
我們看到,函數(shù)bar的[[Scope]]屬性不包含foo上下文的Ao——變量”y”不能訪問,變量”x”從全局對(duì)象中取得。順便提醒一句,F(xiàn)unction構(gòu)造器既可使用new 關(guān)鍵字,也可以沒有,這樣說來,這些變體是等價(jià)的。
這些函數(shù)的其他特點(diǎn)與Equated Grammar Productions 和Joined Objects相關(guān)。作為優(yōu)化建議(但是,實(shí)現(xiàn)上可以不使用優(yōu)化),規(guī)范提供了這些機(jī)制。如,如果我們有一個(gè)100個(gè)元素的數(shù)組,在函數(shù)的一個(gè)循環(huán)中,執(zhí)行可能使用Joined Objects 機(jī)制。結(jié)果是數(shù)組中的所有元素僅一個(gè)函數(shù)對(duì)象可以使用。
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = function () {}; // 可能使用了joined objects
}
但是通過函數(shù)構(gòu)造器創(chuàng)建的函數(shù)不會(huì)被連接。
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = Function(''); // 一直是100個(gè)不同的函數(shù)
}
另外一個(gè)與聯(lián)合對(duì)象(joined objects)相關(guān)的例子:
function foo() {
function bar(z) {
return z * z;
}
return bar;
}
var x = foo();
var y = foo();
這里的實(shí)現(xiàn),也有權(quán)利連接對(duì)象x和對(duì)象y(使用同一個(gè)對(duì)象),因?yàn)楹瘮?shù)(包括它們的內(nèi)部[[Scope]] 屬性)在根本上是沒有區(qū)別的。因此,通過函數(shù)構(gòu)造器創(chuàng)建的函數(shù)總是需要更多的內(nèi)存資源。
下面的偽碼描述了函數(shù)創(chuàng)建的算法(與聯(lián)合對(duì)象相關(guān)的步驟除外)。這些描述有助于你理解ECMAScript中函數(shù)對(duì)象的更多細(xì)節(jié)。這種算法適合所有的函數(shù)類型。
F = new NativeObject();
// 屬性[[Class]]是"Function"
F.[[Class]] = "Function"
// 函數(shù)對(duì)象的原型是Function的原型
F.[[Prototype]] = Function.prototype
// 醫(yī)用到函數(shù)自身
// 調(diào)用表達(dá)式F的時(shí)候激活[[Call]]
// 并且創(chuàng)建新的執(zhí)行上下文
F.[[Call]] = <reference to function>
// 在對(duì)象的普通構(gòu)造器里編譯
// [[Construct]] 通過new關(guān)鍵字激活
// 并且給新對(duì)象分配內(nèi)存
// 然后調(diào)用F.[[Call]]初始化作為this傳遞的新創(chuàng)建的對(duì)象
F.[[Construct]] = internalConstructor
// 當(dāng)前執(zhí)行上下文的作用域鏈
// 例如,創(chuàng)建F的上下文
F.[[Scope]] = activeContext.Scope
// 如果函數(shù)通過new Function(...)來創(chuàng)建,
// 那么
F.[[Scope]] = globalContext.Scope
// 傳入?yún)?shù)的個(gè)數(shù)
F.length = countParameters
// F對(duì)象創(chuàng)建的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, 在循環(huán)里不可枚舉x
F.prototype = __objectPrototype
return F
注意,F(xiàn).[[Prototype]]是函數(shù)(構(gòu)造器)的一個(gè)原型,F(xiàn).prototype是通過這個(gè)函數(shù)創(chuàng)建的對(duì)象的原型(因?yàn)樾g(shù)語常?;靵y,一些文章中F.prototype被稱之為“構(gòu)造器的原型”,這是不正確的)。
這篇文章有些長(zhǎng)。但是,當(dāng)我們?cè)诮酉聛黻P(guān)于對(duì)象和原型章節(jié)中將繼續(xù)討論函數(shù),同樣,我很樂意在評(píng)論中回答您的任何問題。
更多建議: