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

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

函數(shù)(Functions)

介紹

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

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

平時的慣用方式:

function foo() {
  ...
}

或者,下面的函數(shù)為什么要用括號括住?

(function () {
  ...
})();

關(guān)于具體的介紹,早前面的 12 章變量對象和 14 章作用域鏈都有介紹,如果需要詳細了解這些內(nèi)容,請查詢上述 2 個章節(jié)的詳細內(nèi)容。

但我們依然要一個一個分別看看,首先從函數(shù)的類型講起:

函數(shù)類型

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

函數(shù)聲明

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

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

這種函數(shù)類型的主要特點在于它們僅僅影響變量對象(即存儲在上下文的 VO 中的變量對象)。該特點也解釋了第二個重要點(它是變量對象特性的結(jié)果)——在代碼執(zhí)行階段它們已經(jīng)可用(因為 FD 在進入上下文階段已經(jīng)存在于 VO 中——代碼執(zhí)行之前)。

例如(函數(shù)在其聲明之前被調(diào)用)

foo();
function foo() {
  alert('foo');
}

另外一個重點知識點是上述定義中的第二點——函數(shù)聲明在源碼中的位置:

// 函數(shù)可以在如下地方聲明:
// 1) 直接在全局上下文中
function globalFD() {
  // 2) 或者在一個函數(shù)的函數(shù)體內(nèi)
  function innerFD() {}
}

只有這 2 個位置可以聲明函數(shù),也就是說:不可能在表達式位置或一個代碼塊中定義它。

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

函數(shù)表達式

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

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

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

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

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

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

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

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

如果 FE 有一個名稱,就很難與 FD 區(qū)分。但是,如果你明白定義,區(qū)分起來就簡單明了:FE 總是處在表達式的位置。在下面的例子中我們可以看到各種 ECMAScript 表達式:

// 圓括號(分組操作符)內(nèi)只能是表達式
(function foo() {});
// 在數(shù)組初始化器內(nèi)只能是表達式
[function bar() {}];
// 逗號也只能操作表達式
1, function baz() {};

表達式定義里說明:FE 只能在代碼執(zhí)行階段創(chuàng)建而且不存在于變量對象中,讓我們來看一個示例行為:

// FE在定義階段之前不可用(因為它是在代碼執(zhí)行階段創(chuàng)建)
alert(foo); // "foo" 未定義
(function foo() {});
// 定義階段之后也不可用,因為他不在變量對象VO中
alert(foo);  // "foo" 未定義

相當(dāng)一部分問題出現(xiàn)了,我們?yōu)槭裁葱枰瘮?shù)表達式?答案很明顯——在表達式中使用它們,”不會污染”變量對象。最簡單的例子是將一個函數(shù)作為參數(shù)傳遞給其它函數(shù)。

function foo(callback) {
  callback();
}
foo(function bar() {
  alert('foo.bar');
});
foo(function baz() {
  alert('foo.baz');
});

在上述例子里,F(xiàn)E 賦值給了一個變量(也就是參數(shù)),函數(shù)將該表達式保存在內(nèi)存中,并通過變量名來訪問(因為變量影響變量對象),如下:

var foo = function () {
  alert('foo');
};
foo();

另外一個例子是創(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”。同時,“x”在外部不能直接訪問。在許多庫中,這種策略常用來創(chuàng)建”私有”數(shù)據(jù)和隱藏輔助實體。在這種模式中,初始化的FE的名稱通常被忽略:

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

還有一個例子是:在代碼執(zhí)行階段通過條件語句進行創(chuàng)建 FE,不會污染變量對象 VO。

var foo = 10;
var bar = (foo % 2 == 0
  ? function () { alert(0); }
  : function () { alert(1); }
);
bar(); // 0

關(guān)于圓括號的問題

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

按照標(biāo)準,表達式語句不能以一個大括號{開始是因為他很難與代碼塊區(qū)分,同樣,他也不能以函數(shù)關(guān)鍵字開始,因為很難與函數(shù)聲明進行區(qū)分。即,所以,如果我們定義一個立即執(zhí)行的函數(shù),在其創(chuàng)建后立即按以下方式調(diào)用:

function () {
  ...
}();
// 即便有名稱
function foo() {
  ...
}();

我們使用了函數(shù)聲明,上述 2 個定義,解釋器在解釋的時候都會報錯,但是可能有多種原因。

如果在全局代碼里定義(也就是程序級別),解釋器會將它看做是函數(shù)聲明,因為他是以 function 關(guān)鍵字開頭,第一個例子,我們會得到 SyntaxError 錯誤,是因為函數(shù)聲明沒有名字(我們前面提到了函數(shù)聲明必須有名字)。

第二個例子,我們有一個名稱為 foo 的一個函數(shù)聲明正常創(chuàng)建,但是我們依然得到了一個語法錯誤——沒有任何表達式的分組操作符錯誤。在函數(shù)聲明后面他確實是一個分組操作符,而不是一個函數(shù)調(diào)用所使用的圓括號。所以如果我們聲明如下代碼:

// "foo" 是一個函數(shù)聲明,在進入上下文的時候創(chuàng)建
alert(foo); // 函數(shù)
function foo(x) {
  alert(x);
}(1); // 這只是一個分組操作符,不是函數(shù)調(diào)用!
foo(10); // 這才是一個真正的函數(shù)調(diào)用,結(jié)果是10

上述代碼是沒有問題的,因為聲明的時候產(chǎn)生了 2 個對象:一個函數(shù)聲明,一個帶有 1 的分組操作,上面的例子可以理解為如下代碼:

// 函數(shù)聲明
function foo(x) {
  alert(x);
}
// 一個分組操作符,包含一個表達式1
(1);
// 另外一個操作符,包含一個function表達式
(function () {});
// 這個操作符里,包含的也是一個表達式"foo"
("foo");
// 等等

如果我們定義一個如下代碼(定義里包含一個語句),我們可能會說,定義歧義,會得到報錯:

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

根據(jù)規(guī)范,上述代碼是錯誤的(一個表達式語句不能以 function 關(guān)鍵字開頭),但下面的例子就沒有報錯,想想為什么?

我們?nèi)绻麃砀嬖V解釋器:我就像在函數(shù)聲明之后立即調(diào)用,答案是很明確的,你得聲明函數(shù)表達式 function expression,而不是函數(shù)聲明 function declaration,并且創(chuàng)建表達式最簡單的方式就是用分組操作符括號,里邊放入的永遠是表達式,所以解釋器在解釋的時候就不會出現(xiàn)歧義。在代碼執(zhí)行階段這個的 function 就會被創(chuàng)建,并且立即執(zhí)行,然后自動銷毀(如果沒有引用的話)。

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

上述代碼就是我們所說的在用括號括住一個表達式,然后通過(1)去調(diào)用。

注意,下面一個立即執(zhí)行的函數(shù),周圍的括號不是必須的,因為函數(shù)已經(jīng)處在表達式的位置,解析器知道它處理的是在函數(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 是一個字符串而不是一個函數(shù),這里的函數(shù)僅僅用來根據(jù)條件參數(shù)初始化這個屬性——它創(chuàng)建后并立即調(diào)用。

因此,”關(guān)于圓括號”問題完整的答案如下:當(dāng)函數(shù)不在表達式的位置的時候,分組操作符圓括號是必須的——也就是手工將函數(shù)轉(zhuǎn)化成 FE。
如果解析器知道它處理的是 FE,就沒必要用圓括號。

除了大括號以外,如下形式也可以將函數(shù)轉(zhuǎn)化為 FE類型,例如:

// 注意是1,后面的聲明
1, function () {
  alert('anonymous function is called');
}();
// 或者這個
!function () {
  alert('ECMAScript');
}();
// 其它手工轉(zhuǎn)化的形式
...

但是,在這個例子中,圓括號是最簡潔的方式。

順便提一句,組表達式包圍函數(shù)描述可以沒有調(diào)用圓括號,也可包含調(diào)用圓括號,即,下面的兩個表達式都是正確的 FE。

實現(xiàn)擴展:函數(shù)語句

下面的代碼,根據(jù)貴方任何一個 function 聲明都不應(yīng)該被執(zhí)行:

if (true) {
  function foo() {
    alert(0);
  }
} else {
  function foo() {
    alert(1);
  }
}
foo(); // 1 or 0 ?實際在上不同環(huán)境下測試得出個結(jié)果不一樣

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

因為代碼塊僅包含語句,所以這是不正確的??梢猿霈F(xiàn)在塊中的函數(shù)的唯一位置是這些語句中的一個——上面已經(jīng)討論過的表達式語句。但是,按照定義它不能以大括號開始(既然它有別于代碼塊)或以一個函數(shù)關(guān)鍵字開始(既然它有別于 FD)。

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

if-else 分支語句的出現(xiàn)意味著一個動態(tài)的選擇。即,從邏輯上來說,它應(yīng)該是在代碼執(zhí)行階段動態(tài)創(chuàng)建的函數(shù)表達式(FE)。但是,大多數(shù)執(zhí)行在進入上下文階段時簡單的創(chuàng)建函數(shù)聲明(FD),并使用最后聲明的函數(shù)。即,函數(shù) foo 將顯示”1″,事實上 else 分支將永遠不會執(zhí)行。

但是,SpiderMonkey (和TraceMonkey)以兩種方式對待這種情況:一方面它不會將函數(shù)作為聲明處理(即,函數(shù)在代碼執(zhí)行階段根據(jù)條件創(chuàng)建),但另一方面,既然沒有括號包圍(再次出現(xiàn)解析錯誤——”與 FD 有別”),他們不能被調(diào)用,所以也不是真正的函數(shù)表達式,它儲存在變量對象中。

我個人認為這個例子中 SpiderMonkey 的行為是正確的,拆分了它自身的函數(shù)中間類型——(FE+FD)。這些函數(shù)在合適的時間創(chuàng)建,根據(jù)條件,也不像 FE,倒像一個可以從外部調(diào)用的 FD,SpiderMonkey 將這種語法擴展 稱之為函數(shù)語句(縮寫為 FS);該語法在 MDC 中提及過。

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

當(dāng)函數(shù)表達式 FE 有一個名稱(稱為命名函數(shù)表達式,縮寫為 NFE)時,將會出現(xiàn)一個重要的特點。從定義(正如我們從上面示例中看到的那樣)中我們知道函數(shù)表達式不會影響一個上下文的變量對象(那樣意味著既不可能通過名稱在函數(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”儲存在什么地方?在 foo 的活動對象中?不是,因為在 foo 中沒有定義任何”foo”。在上下文的父變量對象中創(chuàng)建 foo?也不是,因為按照定義——FE 不會影響 VO(變量對象)——從外部調(diào)用 foo 我們可以實實在在的看到。那么在哪里呢?

以下是關(guān)鍵點。當(dāng)解釋器在代碼執(zhí)行階段遇到命名的 FE 時,在 FE 創(chuàng)建之前,它創(chuàng)建了輔助的特定對象,并添加到當(dāng)前作用域鏈的最前端。然后它創(chuàng)建了 FE,此時(正如我們在第四章 作用域鏈知道的那樣)函數(shù)獲取了[[Scope]] 屬性——創(chuàng)建這個函數(shù)上下文的作用域鏈)。此后,F(xiàn)E 的名稱添加到特定對象上作為唯一的屬性;這個屬性的值是引用到FE上。最后一步是從父作用域鏈中移除那個特定的對象。讓我們在偽碼中看看這個算法:

specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // 從作用域鏈中刪除定義的特殊對象specialObject

因此,在函數(shù)外部這個名稱不可用的(因為它不在父作用域鏈中),但是,特定對象已經(jīng)存儲在函數(shù)的[[scope]]中,在那里名稱是可用的。

但是需要注意的是一些實現(xiàn)(如 Rhino)不是在特定對象中而是在 FE 的激活對象中存儲這個可選的名稱。Microsoft 中的執(zhí)行完全打破了 FE 規(guī)則,它在父變量對象中保持了這個名稱,這樣函數(shù)在外部變得可以訪問。

NFE 與 SpiderMonkey

我們來看看 NFE 和 SpiderMonkey 的區(qū)別,SpiderMonkey 的一些版本有一個與特定對象相關(guān)的屬性,它可以作為 bug 來對待(雖然按照標(biāo)準所有的都那樣實現(xiàn)了,但更像一個 ECMAScript 標(biāo)準上的 bug)。它與標(biāo)識符的解析機制相關(guān):作用域鏈的分析是二維的,在標(biāo)識符的解析中,同樣考慮到作用域鏈中每個對象的原型鏈。

如果我們在 Object.prototype 中定義一個屬性,并引用一個”不存在(nonexistent)”的變量。我們就能看到這種執(zhí)行機制。這樣,在下面示例的”x”解析中,我們將到達全局對象,但是沒發(fā)現(xiàn)”x”。但是,在 SpiderMonkey 中全局對象繼承了 Object.prototype 中的屬性,相應(yīng)地,”x”也能被解析。

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

活動對象沒有原型。按照同樣的起始條件,在上面的例子中,不可能看到內(nèi)部函數(shù)的這種行為。如果定義一個局部變量”x”,并定義內(nèi)部函數(shù)(FD 或匿名的 FE),然后再內(nèi)部函數(shù)中引用”x”。那么這個變量將在父函數(shù)上下文(即,應(yīng)該在哪里被解析)中而不是在 Object.prototype 中被解析。

Object.prototype.x = 10;
function foo() {
  var x = 20;
  // 函數(shù)聲明
  function bar() {
    alert(x);
  }
  bar(); // 20, 從foo的變量對象AO中查詢
  // 匿名函數(shù)表達式也是一樣
  (function () {
    alert(x); // 20, 也是從foo的變量對象AO中查詢
  })();
}
foo();

盡管如此,一些執(zhí)行會出現(xiàn)例外,它給活動對象設(shè)置了一個原型。因此,在 Blackberry 的執(zhí)行中,上面例子中的”x”被解析為”10″。也就是說,既然在 Object.prototype 中已經(jīng)找到了 foo 的值,那么它就不會到達 foo 的活動對象。

AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10

在 SpiderMonkey 中,同樣的情形我們完全可以在命名 FE 的特定對象中看到。這個特定的對象(按照標(biāo)準)是普通對象——“就像表達式 new Object()”,相應(yīng)地,它應(yīng)該從 Object.prototype 繼承屬性,這恰恰是我們在 SpiderMonkey (1.7 以上的版本)看到的執(zhí)行。其余的執(zhí)行(包括新的 TraceMonkey)不會為特定的對象設(shè)置一個原型。

function foo() {
  var x = 10;
  (function bar() {
    alert(x); // 20, 不上10,不是從foo的活動對象上得到的
    // "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ù)表達式(NFE)相關(guān)的 bug。所有的這些 bug 都完全與 ECMA-262-3 標(biāo)準矛盾;有些可能會導(dǎo)致嚴重的錯誤。

首先,這個例子中 JScript 破壞了 FE 的主要規(guī)則,它不應(yīng)該通過函數(shù)名存儲在變量對象中??蛇x的 FE 名稱應(yīng)該存儲在特定的對象中,并只能在函數(shù)自身(而不是別的地方)中訪問。但IE直接將它存儲在父變量對象中。此外,命名的 FE 在 JScript 中作為函數(shù)聲明(FD)對待。即創(chuàng)建于進入上下文的階段,在源代碼中的定義之前可以訪問。

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

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

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

var foo = function bar() {
  alert('foo');
};
alert(typeof bar); // "function", 
// 有趣的是
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // 未定義
// 但執(zhí)行的時候結(jié)果一樣
foo(); // "foo"
bar(); // "foo"

再次看到,已經(jīng)亂成一片了。

但是,需要注意的是,如果與變量賦值分開,單獨描述 NFE(如通過組運算符),然后將它賦給一個變量,并檢查其相等性,結(jié)果為 true,就好像是一個對象。

(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10

此時是可以解釋的。實際上,再次創(chuàng)建兩個對象,但那樣做事實上仍保持一個。如果我們再次認為這里的 NFE 被作為 FD 對待,然后在進入上下文階段創(chuàng)建 FD bar。此后,在代碼執(zhí)行階段第二個對象——函數(shù)表達式(FE)bar 被創(chuàng)建,它不會被存儲。相應(yīng)地,沒有 FE bar 的任何引用,它被移除了。這樣就只有一個對象——FD bar,對它的引用賦給了變量 foo。

第三,就通過 arguments.callee 間接引用一個函數(shù)而言,它引用的是被激活的那個對象的名稱(確切的說——再這里有兩個函數(shù)對象。

var foo = function bar() {
  alert([
    arguments.callee === foo,
    arguments.callee === bar
  ]);
};
foo(); // [true, false]
bar(); // [false, true]

第四,JScript 像對待普通的 FD 一樣對待 NFE,他不服從條件表達式規(guī)則。即,就像一個 FD,NFE 在進入上下文時創(chuàng)建,在代碼中最后的定義被使用。

var foo = function bar() {
  alert(1);
};
if (false) {
  foo = function bar() {
    alert(2);
  };
}
bar(); // 2
foo(); // 1

這種行為從”邏輯上”也可以解釋。在進入上下文階段,最后遇到的 FD bar 被創(chuàng)建,即包含 alert(2)的函數(shù)。此后,在代碼執(zhí)行階段,新的函數(shù)——FE bar 創(chuàng)建,對它的引用賦給了變量 foo。這樣 foo 激活產(chǎn)生 alert(1)。邏輯很清楚,但考慮到 IE 的 bug,既然執(zhí)行明顯被破壞,并依賴于 JScript 的 bug,我給單詞”邏輯上(logically)”加上了引號。

JScript 的第五個 bug 與全局對象的屬性創(chuàng)建相關(guān),全局對象由賦值給一個未限定的標(biāo)識符(即,沒有 var 關(guān)鍵字)來生成。既然 NFE 在這被作為 FD 對待,相應(yīng)地,它存儲在變量對象中,賦給一個未限定的標(biāo)識符(即不是賦給變量而是全局對象的普通屬性),萬一函數(shù)的名稱與未限定的標(biāo)識符相同,這樣該屬性就不是全局的了。

(function () {
  // 不用var的話,就不是當(dāng)前上下文的一個變量了
  // 而是全局對象的一個屬性
  foo = function foo() {};
})();
//  但,在匿名函數(shù)的外部,foo這個名字是不可用的
alert(typeof foo); // 未定義

“邏輯”已經(jīng)很清楚了:在進入上下文階段,函數(shù)聲明foo取得了匿名函數(shù)局部上下文的活動對象。在代碼執(zhí)行階段,名稱 foo 在 AO 中已經(jīng)存在,即,它被作為局部變量。相應(yīng)地,在賦值操作中,只是簡單的更新已存在于 AO 中的屬性 foo,而不是按照 ECMA-262-3 的邏輯創(chuàng)建全局對象的新屬性。

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

既然這種函數(shù)對象也有自己的特色,我們將它與FD和FE區(qū)分開來。其主要特點在于這種函數(shù)的[[Scope]]屬性僅包含全局對象:

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”從全局對象中取得。順便提醒一句,F(xiàn)unction 構(gòu)造器既可使用 new 關(guān)鍵字,也可以沒有,這樣說來,這些變體是等價的。

這些函數(shù)的其他特點與 Equated Grammar Productions 和 Joined Objects 相關(guān)。作為優(yōu)化建議(但是,實現(xiàn)上可以不使用優(yōu)化),規(guī)范提供了這些機制。如,如果我們有一個 100 個元素的數(shù)組,在函數(shù)的一個循環(huán)中,執(zhí)行可能使用 Joined Objects 機制。結(jié)果是數(shù)組中的所有元素僅一個函數(shù)對象可以使用。

var a = [];
for (var k = 0; k < 100; k++) {
  a[k] = function () {}; // 可能使用了joined objects
}

但是通過函數(shù)構(gòu)造器創(chuàng)建的函數(shù)不會被連接。

var a = [];
for (var k = 0; k < 100; k++) {
  a[k] = Function(''); // 一直是100個不同的函數(shù)
}

另外一個與聯(lián)合對象(joined objects)相關(guān)的例子:

function foo() {
  function bar(z) {
    return z * z;
  }
  return bar;
}
var x = foo();
var y = foo();

這里的實現(xiàn),也有權(quán)利連接對象x和對象y(使用同一個對象),因為函數(shù)(包括它們的內(nèi)部[[Scope]] 屬性)在根本上是沒有區(qū)別的。因此,通過函數(shù)構(gòu)造器創(chuàng)建的函數(shù)總是需要更多的內(nèi)存資源。

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

下面的偽碼描述了函數(shù)創(chuàng)建的算法(與聯(lián)合對象相關(guān)的步驟除外)。這些描述有助于你理解 ECMAScript 中函數(shù)對象的更多細節(jié)。這種算法適合所有的函數(shù)類型。

F = new NativeObject();
// 屬性[[Class]]是"Function"
F.[[Class]] = "Function"
// 函數(shù)對象的原型是Function的原型
F.[[Prototype]] = Function.prototype 
// 醫(yī)用到函數(shù)自身
// 調(diào)用表達式F的時候激活[[Call]]
// 并且創(chuàng)建新的執(zhí)行上下文
F.[[Call]] = <reference to function>
// 在對象的普通構(gòu)造器里編譯
// [[Construct]] 通過new關(guān)鍵字激活
// 并且給新對象分配內(nèi)存
// 然后調(diào)用F.[[Call]]初始化作為this傳遞的新創(chuàng)建的對象
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ù)的個數(shù)
F.length = countParameters
// F對象創(chuàng)建的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, 在循環(huán)里不可枚舉x
F.prototype = __objectPrototype
return F

注意,F(xiàn).[[Prototype]]是函數(shù)(構(gòu)造器)的一個原型,F(xiàn).prototype 是通過這個函數(shù)創(chuàng)建的對象的原型(因為術(shù)語常常混亂,一些文章中 F.prototype 被稱之為“構(gòu)造器的原型”,這是不正確的)。

結(jié)論

這篇文章有些長。但是,當(dāng)我們在接下來關(guān)于對象和原型章節(jié)中將繼續(xù)討論函數(shù)。

其它參考

  1. Function Definition;
  2. Function Objects.