在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ HTML/ 函數(shù)(Functions)
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計(jì)模式之狀態(tài)模式
JavaScript 核心(晉級(jí)高手必讀篇)
設(shè)計(jì)模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計(jì)模式之中介者模式
設(shè)計(jì)模式之裝飾者模式
設(shè)計(jì)模式之模板方法
設(shè)計(jì)模式之外觀模式
強(qiáng)大的原型和原型鏈
設(shè)計(jì)模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計(jì)模式之適配器模式
設(shè)計(jì)模式之組合模式
設(shè)計(jì)模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點(diǎn)
求值策略
閉包(Closures)
對(duì)象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計(jì)模式之代理模式
變量對(duì)象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計(jì)模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計(jì)模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計(jì)模式之享元模式
設(shè)計(jì)模式之原型模式
根本沒(méi)有“JSON 對(duì)象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
全面解析 Module 模式
對(duì)象創(chuàng)建模式(下篇)
設(shè)計(jì)模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開(kāi)閉原則 OCP
設(shè)計(jì)模式之橋接模式
設(shè)計(jì)模式之策略模式
設(shè)計(jì)模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計(jì)模式之工廠模式

函數(shù)(Functions)

介紹

本章節(jié)我們要著重介紹的是一個(gè)非常常見(jiàn)的 ECMAScript 對(duì)象——函數(shù)(function),我們將詳細(xì)講解一下各種類型的函數(shù)是如何影響上下文的變量對(duì)象以及每個(gè)函數(shù)的作用域鏈都包含什么,以及回答諸如像下面這樣的問(wèn)題:下面聲明的函數(shù)有什么區(qū)別么?(如果有,區(qū)別是什么)。

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ù)的類型講起:

函數(shù)類型

在 ECMAScript 中有三種函數(shù)類型:函數(shù)聲明,函數(shù)表達(dá)式和函數(shù)構(gòu)造器創(chuàng)建的函數(shù)。每一種都有自己的特點(diǎn)。

函數(shù)聲明

函數(shù)聲明(縮寫為 FD)是這樣一種函數(shù):

  1. 有一個(gè)特定的名稱
  2. 在源碼中的位置:要么處于程序級(jí)(Program level),要么處于其它函數(shù)的主體(FunctionBody)中
  3. 在進(jìn)入上下文階段創(chuàng)建
  4. 影響變量對(duì)象
  5. 以下面的方式聲明
function exampleFunc() {
  ...
}

這種函數(shù)類型的主要特點(diǎn)在于它們僅僅影響變量對(duì)象(即存儲(chǔ)在上下文的 VO 中的變量對(duì)象)。該特點(diǎn)也解釋了第二個(gè)重要點(diǎn)(它是變量對(duì)象特性的結(jié)果)——在代碼執(zhí)行階段它們已經(jīng)可用(因?yàn)?FD 在進(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ù),也就是說(shuō):不可能在表達(dá)式位置或一個(gè)代碼塊中定義它。

另外一種可以取代函數(shù)聲明的方式是函數(shù)表達(dá)式,解釋如下:

函數(shù)表達(dá)式

函數(shù)表達(dá)式(縮寫為FE)是這樣一種函數(shù):

  1. 在源碼中須出現(xiàn)在表達(dá)式的位置
  2. 有可選的名稱
  3. 不會(huì)影響變量對(duì)象
  4. 在代碼執(zhí)行階段創(chuàng)建

這種函數(shù)類型的主要特點(diǎn)在于它在源碼中總是處在表達(dá)式的位置。最簡(jiǎn)單的一個(gè)例子就是一個(gè)賦值聲明:

var foo = function () {
  ...
};

該例演示是讓一個(gè)匿名函數(shù)表達(dá)式賦值給變量 foo,然后該函數(shù)可以用 foo這個(gè)名稱進(jìn)行訪問(wèn)——foo()。

同時(shí)和定義里描述的一樣,函數(shù)表達(dá)式也可以擁有可選的名稱:

var foo = function _foo() {
  ...
};

需要注意的是,在外部FE通過(guò)變量“foo”來(lái)訪問(wèn)——foo(),而在函數(shù)內(nèi)部(如遞歸調(diào)用),有可能使用名稱“_foo”。

如果 FE 有一個(gè)名稱,就很難與 FD 區(qū)分。但是,如果你明白定義,區(qū)分起來(lái)就簡(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á)式定義里說(shuō)明:FE 只能在代碼執(zhí)行階段創(chuàng)建而且不存在于變量對(duì)象中,讓我們來(lái)看一個(gè)示例行為:

// FE在定義階段之前不可用(因?yàn)樗窃诖a執(zhí)行階段創(chuàng)建)
alert(foo); // "foo" 未定義
(function foo() {});
// 定義階段之后也不可用,因?yàn)樗辉谧兞繉?duì)象VO中
alert(foo);  // "foo" 未定義

相當(dāng)一部分問(wèn)題出現(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)存中,并通過(guò)變量名來(lái)訪問(wèn)(因?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(通過(guò)[[Scope]]屬性)訪問(wèn)到函數(shù) initialize 的內(nèi)部變量“x”。同時(shí),“x”在外部不能直接訪問(wèn)。在許多庫(kù)中,這種策略常用來(lái)創(chuàng)建”私有”數(shù)據(jù)和隱藏輔助實(shí)體。在這種模式中,初始化的FE的名稱通常被忽略:

(function () {
   // 初始化作用域 
})();

還有一個(gè)例子是:在代碼執(zhí)行階段通過(guò)條件語(yǔ)句進(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)的問(wèn)題

讓我們回頭并回答在文章開(kāi)頭提到的問(wèn)題——”為何在函數(shù)創(chuàng)建后的立即調(diào)用中必須用圓括號(hào)來(lái)包圍它?”,答案就是:表達(dá)式句子的限制就是這樣的。

按照標(biāo)準(zhǔn),表達(dá)式語(yǔ)句不能以一個(gè)大括號(hào){開(kāi)始是因?yàn)樗茈y與代碼塊區(qū)分,同樣,他也不能以函數(shù)關(guān)鍵字開(kāi)始,因?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)樗且?function 關(guān)鍵字開(kāi)頭,第一個(gè)例子,我們會(huì)得到 SyntaxError 錯(cuò)誤,是因?yàn)楹瘮?shù)聲明沒(méi)有名字(我們前面提到了函數(shù)聲明必須有名字)。

第二個(gè)例子,我們有一個(gè)名稱為 foo 的一個(gè)函數(shù)聲明正常創(chuàng)建,但是我們依然得到了一個(gè)語(yǔ)法錯(cuò)誤——沒(méi)有任何表達(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

上述代碼是沒(méi)有問(wèn)題的,因?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è)語(yǔ)句),我們可能會(huì)說(shuō),定義歧義,會(huì)得到報(bào)錯(cuò):

if (true) function foo() {alert(1)}

根據(jù)規(guī)范,上述代碼是錯(cuò)誤的(一個(gè)表達(dá)式語(yǔ)句不能以 function 關(guān)鍵字開(kāi)頭),但下面的例子就沒(méi)有報(bào)錯(cuò),想想為什么?

我們?nèi)绻麃?lái)告訴解釋器:我就像在函數(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)銷毀(如果沒(méi)有引用的話)。

(function foo(x) {
  alert(x);
})(1); // 這才是調(diào)用,不是分組操作符

上述代碼就是我們所說(shuō)的在用括號(hào)括住一個(gè)表達(dá)式,然后通過(guò)(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ù)僅僅用來(lái)根據(jù)條件參數(shù)初始化這個(gè)屬性——它創(chuàng)建后并立即調(diào)用。

因此,”關(guān)于圓括號(hào)”問(wèn)題完整的答案如下:當(dāng)函數(shù)不在表達(dá)式的位置的時(shí)候,分組操作符圓括號(hào)是必須的——也就是手工將函數(shù)轉(zhuǎn)化成 FE。
如果解析器知道它處理的是 FE,就沒(méi)必要用圓括號(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ù)描述可以沒(méi)有調(diào)用圓括號(hào),也可包含調(diào)用圓括號(hào),即,下面的兩個(gè)表達(dá)式都是正確的 FE。

實(shí)現(xiàn)擴(kuò)展:函數(shù)語(yǔ)句

下面的代碼,根據(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é)果不一樣

這里有必要說(shuō)明的是,按照標(biāo)準(zhǔn),這種句法結(jié)構(gòu)通常是不正確的,因?yàn)槲覀冞€記得,一個(gè)函數(shù)聲明(FD)不能出現(xiàn)在代碼塊中(這里 if 和 else 包含代碼塊)。我們?cè)?jīng)講過(guò),F(xiàn)D 僅出現(xiàn)在兩個(gè)位置:程序級(jí)(Program level)或直接位于其它函數(shù)體中。

因?yàn)榇a塊僅包含語(yǔ)句,所以這是不正確的。可以出現(xiàn)在塊中的函數(shù)的唯一位置是這些語(yǔ)句中的一個(gè)——上面已經(jīng)討論過(guò)的表達(dá)式語(yǔ)句。但是,按照定義它不能以大括號(hào)開(kāi)始(既然它有別于代碼塊)或以一個(gè)函數(shù)關(guān)鍵字開(kāi)始(既然它有別于 FD)。

但是,在標(biāo)準(zhǔn)的錯(cuò)誤處理章節(jié)中,它允許程序語(yǔ)法的擴(kuò)展執(zhí)行。這樣的擴(kuò)展之一就是我們見(jiàn)到的出現(xiàn)在代碼塊中的函數(shù)。在這個(gè)例子中,現(xiàn)今的所有存在的執(zhí)行都不會(huì)拋出異常,都會(huì)處理它。但是它們都有自己的方式。

if-else 分支語(yǔ)句的出現(xiàn)意味著一個(gè)動(dòng)態(tài)的選擇。即,從邏輯上來(lái)說(shuō),它應(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)建),但另一方面,既然沒(méi)有括號(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 將這種語(yǔ)法擴(kuò)展 稱之為函數(shù)語(yǔ)句(縮寫為 FS);該語(yǔ)法在 MDC 中提及過(guò)。

命名函數(shù)表達(dá)式的特性

當(dāng)函數(shù)表達(dá)式 FE 有一個(gè)名稱(稱為命名函數(shù)表達(dá)式,縮寫為 NFE)時(shí),將會(huì)出現(xiàn)一個(gè)重要的特點(diǎn)。從定義(正如我們從上面示例中看到的那樣)中我們知道函數(shù)表達(dá)式不會(huì)影響一個(gè)上下文的變量對(duì)象(那樣意味著既不可能通過(guò)名稱在函數(shù)聲明之前調(diào)用它,也不可能在聲明之后調(diào)用它)。但是,F(xiàn)E 在遞歸調(diào)用中可以通過(guò)名稱調(diào)用自身。

(function foo(bar) {
  if (bar) {
    return;
  }
  foo(true); // "foo" 是可用的
})();
// 在外部,是不可用的 
foo(); // "foo" 未定義

“foo”儲(chǔ)存在什么地方?在 foo 的活動(dòng)對(duì)象中?不是,因?yàn)樵?foo 中沒(méi)有定義任何”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ù)在外部變得可以訪問(wèn)。

NFE 與 SpiderMonkey

我們來(lái)看看 NFE 和 SpiderMonkey 的區(qū)別,SpiderMonkey 的一些版本有一個(gè)與特定對(duì)象相關(guān)的屬性,它可以作為 bug 來(lái)對(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è)?Object.prototype 中定義一個(gè)屬性,并引用一個(gè)”不存在(nonexistent)”的變量。我們就能看到這種執(zhí)行機(jī)制。這樣,在下面示例的”x”解析中,我們將到達(dá)全局對(duì)象,但是沒(méi)發(fā)現(xiàn)”x”。但是,在 SpiderMonkey 中全局對(duì)象繼承了 Object.prototype 中的屬性,相應(yīng)地,”x”也能被解析。

Object.prototype.x = 10;
(function () {
  alert(x); // 10
})();

活動(dòng)對(duì)象沒(méi)有原型。按照同樣的起始條件,在上面的例子中,不可能看到內(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″。也就是說(shuō),既然在 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è)?SpiderMonkey (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)該通過(guò)函數(shù)名存儲(chǔ)在變量對(duì)象中??蛇x的 FE 名稱應(yīng)該存儲(chǔ)在特定的對(duì)象中,并只能在函數(shù)自身(而不是別的地方)中訪問(wèn)。但I(xiàn)E直接將它存儲(chǔ)在父變量對(duì)象中。此外,命名的 FE 在 JScript 中作為函數(shù)聲明(FD)對(duì)待。即創(chuàng)建于進(jìn)入上下文的階段,在源代碼中的定義之前可以訪問(wèn)。

// FE 在變量對(duì)象里可見(jiàn)
testNFE();
(function testNFE() {
  alert('testNFE');
});
// FE 在定義結(jié)束以后也可見(jiàn)
// 就像函數(shù)聲明一樣
testNFE();

正如我們所見(jiàn),它完全違背了規(guī)則。

其次,在聲明中將命名FE賦給一個(gè)變量時(shí),JScript 創(chuàng)建了兩個(gè)不同的函數(shù)對(duì)象。邏輯上(特別注意的是在 NFE 的外部它的名稱根本不應(yīng)該被訪問(wèn))很難命名這種行為。

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)亂成一片了。

但是,需要注意的是,如果與變量賦值分開(kāi),單獨(dú)描述 NFE(如通過(guò)組運(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)地,沒(méi)有 FE bar 的任何引用,它被移除了。這樣就只有一個(gè)對(duì)象——FD bar,對(duì)它的引用賦給了變量 foo。

第三,就通過(guò) arguments.callee 間接引用一個(gè)函數(shù)而言,它引用的是被激活的那個(gè)對(duì)象的名稱(確切的說(shuō)——再這里有兩個(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í)符(即,沒(méi)有 var 關(guān)鍵字)來(lái)生成。既然 NFE 在這被作為 FD 對(duì)待,相應(yīng)地,它存儲(chǔ)在變量對(duì)象中,賦給一個(gè)未限定的標(biāo)識(shí)符(即不是賦給變量而是全局對(duì)象的普通屬性),萬(wàn)一函數(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ì)象的新屬性。

通過(guò)函數(shù)構(gòu)造器創(chuàng)建的函數(shù)

既然這種函數(shù)對(duì)象也有自己的特色,我們將它與FD和FE區(qū)分開(kāi)來(lái)。其主要特點(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”不能訪問(wèn),變量”x”從全局對(duì)象中取得。順便提醒一句,F(xiàn)unction 構(gòu)造器既可使用 new 關(guān)鍵字,也可以沒(méi)有,這樣說(shuō)來(lái),這些變體是等價(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
}

但是通過(guò)函數(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]] 屬性)在根本上是沒(méi)有區(qū)別的。因此,通過(guò)函數(shù)構(gòu)造器創(chuàng)建的函數(shù)總是需要更多的內(nèi)存資源。

創(chuàng)建函數(shù)的算法

下面的偽碼描述了函數(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]] 通過(guò)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ù)通過(guò)new Function(...)來(lái)創(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 是通過(guò)這個(gè)函數(shù)創(chuàng)建的對(duì)象的原型(因?yàn)樾g(shù)語(yǔ)常?;靵y,一些文章中 F.prototype 被稱之為“構(gòu)造器的原型”,這是不正確的)。

結(jié)論

這篇文章有些長(zhǎng)。但是,當(dāng)我們?cè)诮酉聛?lái)關(guān)于對(duì)象和原型章節(jié)中將繼續(xù)討論函數(shù)。

其它參考

  1. Function Definition;
  2. Function Objects.