摘要:注意,下面一個(gè)立即執(zhí)行的函數(shù),周圍的括號(hào)不是必須的,因?yàn)楹瘮?shù)已經(jīng)處在表達(dá)式的位置,解析器知道它處理的是在函數(shù)執(zhí)行階段應(yīng)該被創(chuàng)建的,這樣在函數(shù)創(chuàng)建后立即調(diào)用了函數(shù)。
本文是翻譯http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#introduction
概要
In this article we will talk about one of the general ECMAScript objects — about functions. In particular, we will go through various types of functions, will define how each type influencesvariables object of a context and what is contained in the scope chain of each function. We will answer the frequently asked questions such as: “is there any difference (and if there are, what are they?) between functions created as follows:
在這一章節(jié)中,我們來探討下ECMAScript中一個(gè)很重要的對(duì)象-函數(shù)。我們將詳細(xì)講解一下各種類型的函數(shù)是如何影響上下文的變量對(duì)象以及每個(gè)函數(shù)的作用域鏈都包含什么,我們將回答諸如像下面這樣的問題:下面聲明的函數(shù)有什么區(qū)別么?(如果有,區(qū)別是什么)。
var foo = function () { ... };
from functions defined in a “habitual” way?”:
傳統(tǒng)的函數(shù)聲明是:
function foo() { ... }
Or, “why in the next call, the function has to be surrounded with parentheses?”:
或者,下面的函數(shù)調(diào)用,為什么要用括號(hào)包圍起來。
(function () { ... })();
Since these articles relay on earlier chapters, for full understanding of this part it is desirable to read Chatper 2. Variable object and Chapter 4. Scope chain, since we will actively use terminology from these chapters.
But let us give one after another. We begin with consideration of function types.
函數(shù)類型
In ECMAScript there are three function types and each of them has its own features.
在ECMAScript中,有三種不同的函數(shù)類型,并且他們都有自己的特點(diǎn)。
函數(shù)聲明
A Function Declaration (abbreviated form is FD) is a function which:
函數(shù)聲明(簡(jiǎn)寫FD)是這樣的一個(gè)函數(shù)
has an obligatory name;
in the source code position it is positioned: either at the Program level or directly in the body of another function (FunctionBody);
is created on entering the context stage;
influences variable object;
and is declared in the following way:
有一個(gè)特定的名稱
在源碼中的位置:要么處于程序級(jí)(Program level),要么處于其它函數(shù)的主體(FunctionBody)中
在進(jìn)入上下文階段創(chuàng)建
影響變量對(duì)象
以下面的方式聲明
function exampleFunc() { ... }
The main feature of this type of functions is that only they influence variable object (they are stored in the VO of the context). This feature defines the second important point (which is a consequence of a variable object nature) — at the code execution stage they are already available (since FD are stored in the VO on entering the context stage — before the execution begins).
這種類型的函數(shù)最重要的特點(diǎn)就是它影響變量對(duì)象(存儲(chǔ)在變量對(duì)象的上下文中),這個(gè)特性也說明了第二個(gè)很重要的觀點(diǎn)(它是變量對(duì)象特性的結(jié)果)在代碼執(zhí)行階段它們已經(jīng)可用(因?yàn)镕D在進(jìn)入上下文階段已經(jīng)存在于VO中——代碼執(zhí)行之前)。
Example (function is called before its declaration in the source code position):
foo(); function foo() { alert("foo"); }
What’s also important is the position at which the funcion is defined in the source code (see the second bullet in the Function declaration definition above):
另外一個(gè)重點(diǎn)知識(shí)點(diǎn)是上述定義中的第二點(diǎn)——函數(shù)聲明在源碼中的位置:
// function can be declared: // 1) directly in the global context function globalFD() { // 2) or inside the body // of another function function innerFD() {} }
These are the only two positions in code where a function may be declared (i.e. it is impossible to declare it in an expression position or inside a code block).
There’s one alternative to function declarations which is called function expressions, which we are about to cover.
只有這2個(gè)位置可以聲明函數(shù),也就是說:不可能在表達(dá)式位置或一個(gè)代碼塊中定義它。
另外一種可以取代函數(shù)聲明的方式是函數(shù)表達(dá)式,解釋如下:
函數(shù)表達(dá)式
A Function Expression (abbreviated form is FE) is a function which:
函數(shù)表達(dá)式(簡(jiǎn)寫FE)是這樣的一個(gè)函數(shù)
in the source code can only be defined at the expression position;
can have an optional name;
it’s definition has no effect on variable object;
and is created at the code execution stage.
在源碼中須出現(xiàn)在表達(dá)式的位置
有可選的名稱
不會(huì)影響變量對(duì)象
在代碼執(zhí)行階段創(chuàng)建
The main feature of this type of functions is that in the source code they are always in theexpression position. Here’s a simple example such assignment expression:
這種函數(shù)類型的主要特點(diǎn)在于它在源碼中總是處在表達(dá)式的位置。最簡(jiǎn)單的一個(gè)例子就是一個(gè)賦值聲明:
var foo = function () { ... };
This example shows how an anonymous FE is assigned to foo variable. After that the function is available via foo name — foo().
The definition states that this type of functions can have an optional name:
該例演示是讓一個(gè)匿名函數(shù)表達(dá)式賦值給變量foo,然后該函數(shù)可以用foo這個(gè)名稱進(jìn)行訪問——foo()。
同時(shí)和定義里描述的一樣,函數(shù)表達(dá)式也可以擁有可選的名稱:
var foo = function _foo() { ... };
What’s important here to note is that from the outside FE is accessible via variable foo — foo(), while from inside the function (for example, in the recursive call), it is also possible to use _fooname.
When a FE is assigned a name it can be difficult to distinguish it from a FD. However, if you know the definition, it is easy to tell them apart: FE is always in the expression position. In the following example we can see various ECMAScript expressions in which all the functions are FE:
需要注意的是,在外部FE通過變量“foo”來訪問——foo(),而在函數(shù)內(nèi)部(如遞歸調(diào)用),有可能使用名稱“_foo”。
如果FE有一個(gè)名稱,就很難與FD區(qū)分。但是,如果你明白定義,區(qū)分起來就簡(jiǎn)單明了:FE總是處在表達(dá)式的位置。在下面的例子中我們可以看到各種ECMAScript 表達(dá)式:
// in parentheses (grouping operator) can be only an expression (function foo() {}); // in the array initialiser – also only expressions [function bar() {}];
// comma also operates with expressions
1, function baz() {};
表達(dá)式定義里說明:FE只能在代碼執(zhí)行階段創(chuàng)建而且不存在于變量對(duì)象中,讓我們來看一個(gè)示例行為:
// FE is not available neither before the definition // (because it is created at code execution phase), alert(foo); // "foo" is not defined (function foo() {}); // nor after, because it is not in the VO alert(foo); // "foo" is not defined
相當(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" is not defined
我們看到函數(shù)foo.bar(通過[[Scope]]屬性)訪問到函數(shù)initialize的內(nèi)部變量“x”。同時(shí),“x”在外部不能直接訪問。在許多庫中,這種策略常用來創(chuàng)建”私有”數(shù)據(jù)和隱藏輔助實(shí)體。在這種模式中,初始化的FE的名稱通常被忽略:
(function () { // initializing scope })();
還有一個(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á)式句子的限制就是這樣的。
根據(jù)標(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 () { ... }(); // or even with a name 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" is a function declaration // and is created on entering the context alert(foo); // function function foo(x) { alert(x); }(1); // and this is just a grouping operator, not a call! foo(10); // and this is already a call, 10
上述代碼是沒有問題的,因?yàn)槁暶鞯臅r(shí)候產(chǎn)生了2個(gè)對(duì)象:一個(gè)函數(shù)聲明,一個(gè)帶有1的分組操作,上面的例子可以理解為如下代碼:
// function declaration function foo(x) { alert(x); } // a grouping operator // with the expression (1); // another grouping operator with // another (function) expression (function () {}); // also - the expression inside ("foo");
根據(jù)規(guī)范,上述代碼是錯(cuò)誤的(一個(gè)表達(dá)式語句不能以function關(guān)鍵字開頭),但下面的例子就沒有報(bào)錯(cuò),想想為什么?
if (true) function foo() {alert(1)}
The construction above by the specification is syntactically incorrect (an expression statement cannot begin with a function keyword), but as we will see below, none of the implementations provide the syntax error, but handle this case, though, every in it’s own manner.
我們?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); // OK, it"s a call, not a grouping operator, 1
上述代碼就是我們所說的在用括號(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)
Apart from surrounding parentheses it is possible to use any other way of transformation of a function to FE type. For example:
除了大括號(hào)以外,如下形式也可以將函數(shù)轉(zhuǎn)化為FE類型,例如:
1,
function () { alert("anonymous function is called"); }(); // or this one !function () { alert("ECMAScript"); }(); // and any other manual // transformation
...
但是,在這個(gè)例子中,圓括號(hào)是最簡(jiǎn)潔的方式。
順便提一句,組表達(dá)式包圍函數(shù)描述可以沒有調(diào)用圓括號(hào),也可包含調(diào)用圓括號(hào),即,下面的兩個(gè)表達(dá)式都是正確的FE。
(function () {})(); (function () {}());
實(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 ? test in different implementations
這里有必要說明的是,按照標(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塊僅包含語句,所以這是不正確的。可以出現(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" name is available })(); // but from the outside, correctly, is not foo(); // "foo" is not defined
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]; // remove specialObject from the front of scope chain
因此,在函數(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
Let’s have a look at how different implementations handle this problem. Some versions of SpiderMonkey have one feature related to special object which can be treated as a bug (although all was implemented according to the standard, so it is more of an editorial defect of the specification). It is related to the mechanism of the identifier resolution: the scope chain analysis istwo-dimensional and when resolving an identifier it considers the prototype chain of every object in the scope chain as well.
說到實(shí)現(xiàn),部分版本的SpiderMonkey有一個(gè)與上述提到的特殊對(duì)象相關(guān)的特性,這個(gè)特性也可以看作是個(gè)bug(既然所有的實(shí)現(xiàn)都是嚴(yán)格遵循標(biāo)準(zhǔn)的,那么這個(gè)就是標(biāo)準(zhǔn)的問題了)。 此特性和標(biāo)識(shí)符處理相關(guān): 作用域鏈的分析是二維的,在標(biāo)識(shí)符查詢的時(shí)候,還要考慮作用域鏈中每個(gè)對(duì)象的原型鏈。
We can see this mechanism in action if we define a property in Object.prototype and use a “nonexistent” variable from the code. In the following example when resolving the name x the global object is reached without finding x. However since in SpiderMonkey the global object inherits from Object.prototype the name x is resolved there:
當(dāng)在Object.prototype對(duì)象上定義一個(gè)屬性,并將該屬性值指向一個(gè)“根本不存在”的變量時(shí),就能夠體現(xiàn)該特性。 比如,如下例子中的變量“x”,在查詢過程中,通過作用域鏈,一直到全局對(duì)象也是找不到“x”的。 然而,在SpiderMonkey中,全局對(duì)象繼承自O(shè)bject.prototype,于是,對(duì)應(yīng)的值就在該對(duì)象中找到了:
Object.prototype.x = 10; (function () { alert(x); // 10 })();
Activation objects do not have prototypes. With the same start conditions, it is possible to see the same behavior in the example with inner function. If we were to define a local variable x and declare inner function (FD or anonymous FE) and then to reference x from the inner function, this variable would be resolved normally in the parent function context (i.e. there, where it should be and is), instead of in Object.prototype:
活躍對(duì)象是沒有原型一說的??梢酝ㄟ^內(nèi)部函數(shù)還證明。 如果在定義一個(gè)局部變量“x”并聲明一個(gè)內(nèi)部函數(shù)(FD或者匿名的FE),然后,在內(nèi)部函數(shù)中引用變量“x”,這個(gè)時(shí)候該變量會(huì)在上層函數(shù)上下文中查詢到(理應(yīng)如此),而不是在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();
Some implementations set a prototype for activation objects, which is an exception compared to most of other implementations. So, in the Blackberry implementation value x from the above example is resolved to 10. I.e. do not reach activation object of foo since value is found in Object.prototype:
在有些實(shí)現(xiàn)中,存在這樣的異常:它們會(huì)在活躍對(duì)象設(shè)置原型。比方說,在Blackberry的實(shí)現(xiàn)中,上述例子中變量“x”值就會(huì)變成10。 因?yàn)?,“x”從Object.prototype中就找到了:
AO(bar FD or anonymous FE) -> no -> AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
當(dāng)出現(xiàn)有名字的FE的特殊對(duì)象的時(shí)候,在SpiderMonkey中也是有同樣的異常。該特殊對(duì)象是常見對(duì)象 —— “和通過new Object()表達(dá)式產(chǎn)生的一樣”。 相應(yīng)地,它也應(yīng)當(dāng)繼承自O(shè)bject.prototype,上述描述只針對(duì)SpiderMonkey(1.7版本)。其他的實(shí)現(xiàn)(包括新的TraceMonkey)是不會(huì)給這個(gè)特殊對(duì)象設(shè)置原型的:
function foo() { var x = 10; (function bar() { alert(x); // 20, but not 10, as don"t reach AO(foo) // "x" is resolved by the chain: // AO(bar) - no -> __specialObject(bar) -> no // __specialObject(bar).[[Prototype]] - yes: 20 })(); } Object.prototype.x = 20; foo();
NFE and JScript
ECMAScript implementation from Microsoft — JScript which is currently built into Internet Explorer (up to JScript 5.8 — IE8) has a number of bugs related with named function expressions (NFE). Every of these bugs completely contradicts ECMA-262-3 standard; some of them may cause serious errors.
First, JScript in this case breaks the main rule of FE that they should not be stored in the variable object by name of functions. An optional FE name which should be stored in the special object and be accessible only inside the function itself (and nowhere else) here is stored directly in the parent variable object. Moreover, named FE is treated in JScript as the function declaration (FD), i.e. is created on entering the context stage and is available before the definition in the source code:
微軟的實(shí)現(xiàn)——JScript,是IE的JS引擎(截至本文撰寫時(shí)最新是JScript5.8——IE8),該引擎與NFE相關(guān)的bug有很多。每個(gè)bug基本上都和ECMA-262-3rd標(biāo)準(zhǔn)是完全違背的。 有些甚至?xí)l(fā)嚴(yán)重的錯(cuò)誤。
第一,針對(duì)上述這樣的情況,JScript完全破壞了FE的規(guī)則:不應(yīng)當(dāng)將函數(shù)名字保存在變量對(duì)象中的。 另外,F(xiàn)E的名字應(yīng)當(dāng)保存在特殊對(duì)象中,并且只有在函數(shù)自身內(nèi)部才可以訪問(其他地方均不可以)。而JScript卻將其直接保存在上層上下文的變量對(duì)象中。 并且,JScript居然還將FE以FD的方式處理,在進(jìn)入上下文的時(shí)候就將其創(chuàng)建出來,并在定義之前就可以訪問到:
// FE is available in the variable object // via optional name before the // definition like a FD testNFE(); (function testNFE() { alert("testNFE"); }); // and also after the definition // like FD; optional name is // in the variable object testNFE();
正如大家所見,完全破壞了FE的規(guī)則。
第二,在聲明同時(shí),將NFE賦值給一個(gè)變量的時(shí)候,JScript會(huì)創(chuàng)建兩個(gè)不同的函數(shù)對(duì)象。 這種行為感覺完全不符合邏輯(特別是考慮到在NFE外層,其名字根本是無法訪問到的):
var foo = function bar() { alert("foo"); }; alert(typeof bar); // "function", NFE again in the VO – already mistake // but, further is more interesting alert(foo === bar); // false! foo.x = 10; alert(bar.x); // undefined // but both function make // the same action foo(); // "foo" bar(); // "foo"
然而,要注意的是: 當(dāng)將NFE和賦值給變量這兩件事情分開的話(比如,通過組操作符),在定義好后,再進(jìn)行變量賦值,這樣,兩個(gè)對(duì)象就相同了,返回true:
(function bar() {}); var foo = bar; alert(foo === bar); // true foo.x = 10; alert(bar.x); // 10
這個(gè)時(shí)候就好解釋了。實(shí)施上,一開始的確創(chuàng)建了兩個(gè)對(duì)象,不過之后就只剩下一個(gè)了。這里將NFE以FD的方式來處理,然后,當(dāng)進(jìn)入上下文的時(shí)候,F(xiàn)D bar就創(chuàng)建出來了。 在這之后,到了執(zhí)行代碼階段,又創(chuàng)建出了第二個(gè)對(duì)象 —— FE bar,該對(duì)象不會(huì)進(jìn)行保存。相應(yīng)的,由于沒有變量對(duì)其進(jìn)行引用,隨后FE bar對(duì)象就被移除了。 因此,這里就只剩下一個(gè)對(duì)象——FD bar對(duì)象,對(duì)該對(duì)象的引用就賦值給了foo變量。
第三,通過arguments.callee對(duì)一個(gè)函數(shù)進(jìn)行間接引用,它引用的是和激活函數(shù)名一致的對(duì)象(事實(shí)上是——函數(shù),因?yàn)橛袃蓚€(gè)對(duì)象):
var foo = function bar() { alert([ arguments.callee === foo, arguments.callee === bar ]); }; foo(); // [true, false] bar(); // [false, true]
Fourthly, as JScript treats NFE as usual FD, it is not submitted to conditional operators rules, i.e. just like a FD, NFE is created on entering the context and the last definition in a code is used:
第四,JScript會(huì)將NFE以FD來處理,但當(dāng)遇到條件語句又不遵循此規(guī)則了。比如說,和FD那樣,NFE會(huì)在進(jìn)入上下文的時(shí)候就創(chuàng)建出來,這樣最后一次定義的就會(huì)被使用:
var foo = function bar() { alert(1); }; if (false) { foo = function bar() { alert(2); }; } bar(); // 2 foo(); // 1
上述行為從邏輯上也是可以解釋通的: 當(dāng)進(jìn)入上下文的時(shí)候,最后一次定義的FD bar被創(chuàng)建出來(有alert(2)的函數(shù)), 之后到了執(zhí)行代碼階段又一個(gè)新的函數(shù) —— FE bar被創(chuàng)建出來,對(duì)其引用賦值給了變量foo。因此(if代碼塊中由于判斷條件是false,因此其代碼塊中的代碼永遠(yuǎn)不會(huì)被執(zhí)行到)foo函數(shù)的調(diào)用會(huì)打印出1。 盡管“邏輯上”是對(duì)的,但是這個(gè)仍然算是IE的bug。因?yàn)樗黠@就破壞了實(shí)現(xiàn)的規(guī)則,所以我這里用了引號(hào)“邏輯上”。
第五個(gè)JScript中NFE的bug和通過給一個(gè)未受限的標(biāo)識(shí)符賦值(也就是說,沒有var關(guān)鍵字)來創(chuàng)建全局對(duì)象的屬性相關(guān)。 由于這里NFE會(huì)以FD的方式來處理,并相應(yīng)地會(huì)保存在變量對(duì)象上,賦值給未受限的標(biāo)識(shí)符(不是給變量而是給全局對(duì)象的一般屬性), 當(dāng)函數(shù)名和標(biāo)識(shí)符名字相同的時(shí)候,該屬性就不會(huì)是全局的了。
(function () { // without var not a variable in the local // context, but a property of global object foo = function foo() {}; })(); // however from the outside of // anonymous function, name foo // is not available alert(typeof foo); // undefined
Again, the “l(fā)ogic” is clear: the function declaration foo gets to the activation object of a local context of anonymous function on entering the context stage. And at the moment of code execution stage, the name foo already exists in AO, i.e. is treated as local. Accordingly, at assignment operation there is simply an update of already existing in AO property foo, but not creation of new property of global object as should be according to the logic of ECMA-262-3.
這里從“邏輯上”又是可以解釋通的: 進(jìn)入上下文時(shí),函數(shù)聲明在匿名函數(shù)本地上下文的活躍對(duì)象中。 當(dāng)進(jìn)入執(zhí)行代碼階段的時(shí)候,因?yàn)閒oo這個(gè)名字已經(jīng)在AO中存在了(本地),相應(yīng)地,賦值操作也只是簡(jiǎn)單的對(duì)AO中的foo進(jìn)行更新而已。 并沒有在全局對(duì)象上創(chuàng)建新的屬性。
通過Function構(gòu)造器創(chuàng)建的函數(shù)
This type of function objects is discussed separately from FD and FE since it also has its own features. The main feature is that the [[Scope]] property of such functions contains only global object:
這類函數(shù)有別于FD和FE,有自己的專屬特性: 它們的[[Scope]]屬性中只包含全局對(duì)象:
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 }
We see that the [[Scope]] of bar function does not contain AO of foo context — the variable “y” is not accessible and the variable “x” is taken from the global context. By the way, pay attention, theFunction constructor can be used both with new keyword and without it, in this case these variants are equivalent.
我們看到bar函數(shù)的[[Scope]]屬性并未包含foo上下文的AO —— 變量“y”是無法訪問的,并且變量“x”是來自全局上下文。 順便提下,這里要注意的是,F(xiàn)unction構(gòu)造器可以通過new關(guān)鍵字和省略new關(guān)鍵字兩種用法。上述例子中,這兩種用法都是一樣的。
The other feature of such functions is related with Equated Grammar Productions and Joined Objects. This mechanism is provided by the specification as suggestion for the optimization (however, implementations have the right not to use such optimization). For example, if we have an array of 100 elements which is filled in a loop with functions, then implementation can use this mechanism of joined objects. As a result only one function object for all elements of an array can be used:
此類函數(shù)其他特性則和同類語法產(chǎn)生式以及聯(lián)合對(duì)象有關(guān)。 該機(jī)制在標(biāo)準(zhǔn)中建議在作優(yōu)化的時(shí)候采用(當(dāng)然,具體的實(shí)現(xiàn)者也完全有權(quán)利不使用這類優(yōu)化)。比方說,有100元素的數(shù)組,在循環(huán)數(shù)組過程中會(huì)給數(shù)組每個(gè)元素賦值(函數(shù)), 這個(gè)時(shí)候,實(shí)現(xiàn)的時(shí)候就可以采用聯(lián)合對(duì)象的機(jī)制了。這樣,最終所有的數(shù)組元素都會(huì)引用同一個(gè)函數(shù)(只有一個(gè)函數(shù)):
var a = []; for (var k = 0; k < 100; k++) { a[k] = function () {}; // possibly, joined objects are used }
但是,通過Function構(gòu)造器創(chuàng)建的函數(shù)就無法使用聯(lián)合對(duì)象了:
var a = []; for (var k = 0; k < 100; k++) { a[k] = Function(""); // always 100 different funcitons }
下面是另外一個(gè)和聯(lián)合對(duì)象相關(guān)的例子:
function foo() { function bar(z) { return z * z; } return bar; } var x = foo(); var y = foo();
Here also implementation has the right to join objects x and y (and to use one object) because functions physically (including their internal [[Scope]] property) are not distinguishable. Therefore, the functions created via Function constructor always require more memory resources.
上述例子,在實(shí)現(xiàn)過程中同樣可以使用聯(lián)合對(duì)象。來使得x和y引用同一個(gè)對(duì)象,因?yàn)楹瘮?shù)(包括它們內(nèi)部的[[Scope]]屬性)物理上是不可分辨的。 因此,通過Function構(gòu)造器創(chuàng)建的函數(shù)總是會(huì)占用更多內(nèi)存資源。
函數(shù)創(chuàng)建的算法
The pseudo-code of function creation algorithm (except steps with joined objects) is described below. This description helps to understand in more detail which function objects exist in ECMAScript. The algorithm is identical for all function types.
如下所示使用偽代碼表示的函數(shù)創(chuàng)建的算法(不包含聯(lián)合對(duì)象的步驟)。有助于理解ECMAScript中的函數(shù)對(duì)象。此算法對(duì)所有函數(shù)類型都是一樣的。
復(fù)制代碼
F = new NativeObject(); // 屬性[[Class]] is "Function" F.[[Class]] = "Function" // 函數(shù)對(duì)象的原型 F.[[Prototype]] = Function.prototype // 對(duì)函數(shù)自身的引用 // [[Call]] is activated by call expression F() // 創(chuàng)建一個(gè)新的上下文 F.[[Call]] =// built in general constructor of objects 內(nèi)置構(gòu)造器 // [[Construct]] is activated via "new" keyword [[Construct]]是在new 關(guān)鍵字的時(shí)候激活。 // and it is the one who allocates memory for new 它會(huì)為新對(duì)象申請(qǐng)內(nèi)存 // objects; then it calls F.[[Call]] // to initialize created objects passing as // "this" value newly created object F.[[Construct]] = internalConstructor // scope chain of the current context // i.e. context which creates function F 當(dāng)前上下文的作用域鏈 F.[[Scope]] = activeContext.Scope // if this functions is created // via new Function(...), then 如果是通過new 運(yùn)算符來創(chuàng)建的,則 F.[[Scope]] = globalContext.Scope // number of formal parameters 形參的個(gè)數(shù) F.length = countParameters // a prototype of created by F objects 通過F創(chuàng)建出來的原型 __objectPrototype = new Object(); __objectPrototype.constructor = F // {DontEnum}, is not enumerable in loops F.prototype = __objectPrototype return F
要注意的是,F(xiàn).[[Prototype]]是函數(shù)(構(gòu)造器)的原型,而F.prototype是通過該函數(shù)創(chuàng)建出來的對(duì)象的原型(因?yàn)橥ǔ?duì)這兩個(gè)概念都會(huì)混淆,在有些文章中會(huì)將F.prototype叫做“構(gòu)造器的原型”,這是錯(cuò)誤的)。
結(jié)論
本文介紹了很多關(guān)于函數(shù)的內(nèi)容;不過在后面的關(guān)于對(duì)象和原型的文章中,還會(huì)提到函數(shù)作為構(gòu)造器是如何工作的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/85871.html
摘要:深入淺出的理解問題的由來寫法一寫法二雖然和指向同一個(gè)函數(shù),但是執(zhí)行結(jié)果可能不一樣。該變量由運(yùn)行環(huán)境提供。所以,就出現(xiàn)了,它的設(shè)計(jì)目的就是在函數(shù)體內(nèi)部,指代函數(shù)當(dāng)前的運(yùn)行環(huán)境。 深入淺出this的理解 問題的由來 var obj = { foo: function(){} } var foo = obj.foo; // 寫法一 obj.foo(); // 寫法二 foo...
摘要:下面是用實(shí)現(xiàn)轉(zhuǎn)成抽象語法樹如下還支持繼承以下是轉(zhuǎn)換結(jié)果最終的結(jié)果還是代碼,其中包含庫中的一些函數(shù)??梢允褂眯碌囊子谑褂玫念惗x,但是它仍然會(huì)創(chuàng)建構(gòu)造函數(shù)和分配原型。 這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 15 篇。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們: JavaScript 是...
摘要:否則不會(huì)得到異步之后的值對(duì)象的值,并沒有在中進(jìn)行處理,而是直接作為返回值返回到對(duì)象外面了這就是的魔法。當(dāng)生成器函數(shù)內(nèi)的邏輯執(zhí)行完畢且沒有錯(cuò)誤之后,這個(gè)對(duì)象返回值變?yōu)闋顟B(tài),且將生成器的返回值作為出來的值。 之前我在關(guān)于Promise的文章中提到了co這個(gè)庫。在這篇文章里,我將寫一寫自己對(duì)它的認(rèn)識(shí)。 Trust me,用了co庫,你不想用別的,來它半斤異步調(diào)用你一口能吃仨。 但是我對(duì)Tj大...
摘要:閉包面試題解由于作用域鏈機(jī)制的影響,閉包只能取得內(nèi)部函數(shù)的最后一個(gè)值,這引起的一個(gè)副作用就是如果內(nèi)部函數(shù)在一個(gè)循環(huán)中,那么變量的值始終為最后一個(gè)值。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第8天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了...
摘要:下面我們就羅列閉包的幾個(gè)常見問題,從回答問題的角度來理解和定義你們心中的閉包。函數(shù)可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)內(nèi)部的變量可以保存在其他函數(shù)作用域內(nèi),這種特性在計(jì)算機(jī)科學(xué)文獻(xiàn)中稱為閉包。 寫這篇文章之前,我對(duì)閉包的概念及原理模糊不清,一直以來都是以通俗的外層函數(shù)包裹內(nèi)層....來欺騙自己。并沒有說這種說法的對(duì)與錯(cuò),我只是不想擁有從眾心理或者也可以說如果我們說出更好更低層的東西,逼格...
摘要:譯者注翻譯一個(gè)對(duì)新手比較友好的工作原理解析系列文章注意以下全部是概念經(jīng)驗(yàn)豐富的老鳥可以離場(chǎng)啦正文從這里開始隨著的流行團(tuán)隊(duì)們正在利用來支持多個(gè)級(jí)別的技術(shù)棧包括前端后端混合開發(fā)嵌入式設(shè)備以及更多這篇文章旨在成為深入挖掘和實(shí)際上他是怎么工作的系列 譯者注 翻譯一個(gè)對(duì)新手比較友好的 JavaScript 工作原理解析系列文章 注意: 以下全部是概念,經(jīng)驗(yàn)豐富的老鳥可以離場(chǎng)啦 正文從這里開始 隨...
閱讀 2552·2023-04-25 19:47
閱讀 3396·2019-08-29 17:18
閱讀 861·2019-08-29 15:26
閱讀 3368·2019-08-29 14:17
閱讀 1145·2019-08-26 13:49
閱讀 3346·2019-08-26 13:22
閱讀 3034·2019-08-26 10:44
閱讀 2702·2019-08-23 16:51