本文是翻譯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:
在這一章節中,我們來探討下ECMAScript中一個很重要的對象-函數。我們將詳細講解一下各種類型的函數是如何影響上下文的變量對象以及每個函數的作用域鏈都包含什么,我們將回答諸如像下面這樣的問題:下面聲明的函數有什么區別么?(如果有,區別是什么)。
var
foo =
function
() {
...
};
|
from functions defined in a “habitual” way?”:
傳統的函數聲明是:
function
foo() {
...
}
|
Or, “why in the next call, the function has to be surrounded with parentheses?”:
或者,下面的函數調用,為什么要用括號包圍起來。
(
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.
函數類型
In ECMAScript there are three function types and each of them has its own features.
在ECMAScript中,有三種不同的函數類型,並且他們都有自己的特點。
函數聲明
函數聲明(簡寫FD)是這樣的一個函數
- 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:
- 有一個特定的名稱
- 在源碼中的位置:要么處於程序級(Program level),要么處於其它函數的主體(FunctionBody)中
- 在進入上下文階段創建
- 影響變量對象
- 以下面的方式聲明
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).
這種類型的函數最重要的特點就是它影響變量對象(存儲在變量對象的上下文中),這個特性也說明了第二個很重要的觀點(它是變量對象特性的結果)在代碼執行階段它們已經可用(因為FD在進入上下文階段已經存在於VO中——代碼執行之前)。
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):
另外一個重點知識點是上述定義中的第二點——函數聲明在源碼中的位置:
// 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個位置可以聲明函數,也就是說:不可能在表達式位置或一個代碼塊中定義它。
另外一種可以取代函數聲明的方式是函數表達式,解釋如下:
函數表達式
函數表達式(簡寫FE)是這樣的一個函數
- 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.
- 在源碼中須出現在表達式的位置
- 有可選的名稱
- 不會影響變量對象
- 在代碼執行階段創建
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:
這種函數類型的主要特點在於它在源碼中總是處在表達式的位置。最簡單的一個例子就是一個賦值聲明:
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:
該例演示是讓一個匿名函數表達式賦值給變量foo,然后該函數可以用foo這個名稱進行訪問——foo()。
同時和定義里描述的一樣,函數表達式也可以擁有可選的名稱:
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 _foo
name.
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(),而在函數內部(如遞歸調用),有可能使用名稱“_foo”。
如果FE有一個名稱,就很難與FD區分。但是,如果你明白定義,區分起來就簡單明了:FE總是處在表達式的位置。在下面的例子中我們可以看到各種ECMAScript 表達式:
// 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() {};
|
The definition also states that FE is created at the code execution stage and is not stored in the variable object. Let’s see an example of this behavior:
表達式定義里說明:FE只能在代碼執行階段創建而且不存在於變量對象中,讓我們來看一個示例行為:
// 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
|
The logical question now is why do we need this type of functions at all? The answer is obvious — to use them in expressions and “not pollute” the variables object. This can be demonstrated in passing a function as an argument to another function:
相當一部分問題出現了,我們為什么需要函數表達式?答案是很顯然的——在表達式中使用它們,”不會污染”變量對象。最簡單的例子是將一個函數作為參數傳遞給其它函數。
function
foo(callback) {
callback();
}
foo(
function
bar() {
alert(
'foo.bar'
);
});
foo(
function
baz() {
alert(
'foo.baz'
);
});
|
In case a FE is assigned to a variable, the function remains stored in memory and can later be accessed via this variable name (because variables as we know influence VO):
在上述例子里,FE賦值給了一個變量(也就是參數),函數將該表達式保存在內存中,並通過變量名來訪問(因為變量影響變量對象),如下:
var
foo =
function
() {
alert(
'foo'
);
};
foo();
|
Another example is creation of encapsulated scope to hide auxiliary helper data from external context (in the following example we use FE which is called right after creation):
另外一個例子是創建封裝的閉包從外部上下文中隱藏輔助性數據(在下面的例子中我們使用FE,它在創建后立即調用):
var
foo = {};
(
function
initialize() {
var
x = 10;
foo.bar =
function
() {
alert(x);
};
})();
foo.bar();
// 10;
alert(x);
// "x" is not defined
|
We see that function foo.bar
(via its [[Scope]]
property) has access to the internal variable x
of function initialize
. And at the same time x
is not accessible directly from the outside. This strategy is used in many libraries to create “private” data and hide auxiliary entities. Often in this pattern the name of initializing FE is omitted:
我們看到函數foo.bar(通過[[Scope]]屬性)訪問到函數initialize的內部變量“x”。同時,“x”在外部不能直接訪問。在許多庫中,這種策略常用來創建”私有”數據和隱藏輔助實體。在這種模式中,初始化的FE的名稱通常被忽略:
(
function
() {
// initializing scope
})();
|
Here’s another examples of FE which are created conditionally at runtime and do not pollute VO:
還有一個例子是:在代碼執行階段通過條件語句進行創建FE,不會污染變量對象VO。
var
foo = 10;
var
bar = (foo % 2 == 0
?
function
() { alert(0); }
:
function
() { alert(1); }
);
bar();
// 0
|
關於圓括號的問題
Let’s go back and answer the question from the beginning of the article — “why is it necessary to surround a function in parentheses if we want to call it right from it’s definition”. Here’s an answer to this question: restrictions of the expression statement.
According to the standard, the expression statement (ExpressionStatement) cannot begin with an opening curly brace — {
since it would be indistinguishable from the block, and also the expression statement cannot begin with a function
keyword since then it would be indistinguishable from thefunction declaration. I.e., if we try to define an immediately invoked function the following way (starting with a function
keyword):
讓我們回頭並回答在文章開頭提到的問題——”為何在函數創建后的立即調用中必須用圓括號來包圍它?”,答案就是:表達式句子的限制就是這樣的。
根據標准,表達式語句不能以一個大括號{開始是因為他很難與代碼塊區分,同樣,他也不能以函數關鍵字開始,因為很難與函數聲明進行區分。即,所以,如果我們定義一個立即執行的函數,在其創建后立即按以下方式調用:
function
() {
...
}();
// or even with a name
function
foo() {
...
}();
|
we deal with function declarations, and in both cases a parser will produce a parse error. However, the reasons of these parse errors vary.
If we put such a definition in the global code (i.e. on the Program
level), the parser should treat the function as declaration, since it starts with a function
keyword. And in first case we get aSyntaxError
because of absence of the function’s name (a function declaration as we said should always have a name).
我們使用了函數聲明,上述2個定義,解釋器在解釋的時候都會報錯,但是可能有多種原因。
如果在全局代碼里定義(也就是程序級別),解釋器會將它看做是函數聲明,因為他是以function關鍵字開頭,第一個例子,我們會得到SyntaxError錯誤,是因為函數聲明沒有名字(我們前面提到了函數聲明必須有名字)。
In the second case we do have a name (foo
) and the function declaration should be created normally. But it doesn’t since we have another syntax error there — a grouping operator without an expression inside it. Notice, in this case it’s exactly a grouping operator which follows the function declaration, but not the parentheses of a function call! So if we had the following source:
第二個例子,我們有一個名稱為foo的一個函數聲明正常創建,但是我們依然得到了一個語法錯誤——沒有任何表達式的分組操作符錯誤。在函數聲明后面他確實是一個分組操作符,而不是一個函數調用所使用的圓括號。所以如果我們聲明如下代碼:
// "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
|
everything is fine since here we have two syntactic productions — a function declaration and agrouping operator with an expression (1
) inside it. The example above is the same as:
上述代碼是沒有問題的,因為聲明的時候產生了2個對象:一個函數聲明,一個帶有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"
);
// etc
|
In case we had such a definition inside a statement, then as we said, there because of ambiguity we would get a syntax error:
根據規范,上述代碼是錯誤的(一個表達式語句不能以function關鍵字開頭),但下面的例子就沒有報錯,想想為什么?
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.
Having all this, how should we tell the parser that what we really want it to call a function immediately after its creation? The answer is obvious. It’s should be a function expression, and nota function declaration. And the simplest way to create an expression is to use mentioned abovegrouping operator. Inside it always there is an expression. Thus, the parser distinguishes a code as a function expression (FE) and there is no ambiguity. Such a function will be created during theexecution stage, then executed, and then removed (if there are no references to it).
我們如果來告訴解釋器:我就像在函數聲明之后立即調用,答案是很明確的,你得聲明函數表達式function expression,而不是函數聲明function declaration,並且創建表達式最簡單的方式就是用分組操作符括號,里邊放入的永遠是表達式,所以解釋器在解釋的時候就不會出現歧義。在代碼執行階段這個的function就會被創建,並且立即執行,然后自動銷毀(如果沒有引用的話)。
(
function
foo(x) {
alert(x);
})(1);
// OK, it's a call, not a grouping operator, 1
|
In the example above the parentheses at the end (Arguments
production) are already call of the function, and not a grouping operator as it was in case of a FD.
上述代碼就是我們所說的在用括號括住一個表達式,然后通過(1)去調用。
Notice, in the following example of the immediate invocation of a function, the surrounding parentheses are not required, since the function is already in the expression position and the parser knows that it deals with a FE which should be created at code execution stage:
注意,下面一個立即執行的函數,周圍的括號不是必須的,因為函數已經處在表達式的位置,解析器知道它處理的是在函數執行階段應該被創建的FE,這樣在函數創建后立即調用了函數。
var
foo = {
bar:
function
(x) {
return
x % 2 != 0 ?
'yes'
:
'no'
;
}(1)
};
alert(foo.bar);
// 'yes'
|
As we see, foo.bar
is a string but not a function as can seem at first inattentive glance. The function here is used only for initialization of the property — depending on the conditional parameter — it is created and called right after that.
就像我們看到的,foo.bar是一個字符串而不是一個函數,這里的函數僅僅用來根據條件參數初始化這個屬性——它創建后並立即調用。
Therefore, the complete answer to the question “about parentheses” is the following:
Grouping parentheses are needed when a function is not at the expression position and if we want to call it immediately right after its creation — in this case we just manually transform the function to FE.
In case when a parser knows that it deals with a FE, i.e. the function is already at the expression position — the parentheses are not required.
因此,”關於圓括號”問題完整的答案如下:當函數不在表達式的位置的時候,分組操作符圓括號是必須的——也就是手工將函數轉化成FE。如果解析器知道它處理的是FE,就沒必要用圓括號
Apart from surrounding parentheses it is possible to use any other way of transformation of a function to FE type. For example:
除了大括號以外,如下形式也可以將函數轉化為FE類型,例如:
1,
function
() {
alert(
'anonymous function is called'
);
}();
// or this one
!
function
() {
alert(
'ECMAScript'
);
}();
// and any other manual
// transformation
...
|
However, grouping parentheses are just the most widespread and the elegant way to do it.
By the way, the grouping operator can surround the function description as without call parentheses, and also including call parentheses. I.e. both expressions below are correct FE:
但是,在這個例子中,圓括號是最簡潔的方式。
順便提一句,組表達式包圍函數描述可以沒有調用圓括號,也可包含調用圓括號,即,下面的兩個表達式都是正確的FE。
(
function
() {})();
(
function
() {}());
|
實現擴展: 函數語句
The following example shows a code in which none of implementations processes accordingly to the specification:
下面的代碼,根據貴方任何一個function聲明都不應該被執行:
if
(
true
) {
function
foo() {
alert(0);
}
}
else
{
function
foo() {
alert(1);
}
}
foo();
// 1 or 0 ? test in different implementations
|
Here it is necessary to say that according to the standard this syntactic construction in general isincorrect, because as we remember, a function declaration (FD) cannot appear inside a code block(here if
and else
contain code blocks). As it has been said, FD can appear only in two places: at the Program level or directly inside a body of another function.
The above example is incorrect because the code block can contain only statements. And the only place in which function can appear within a block is one of such statements — the expression statement. But by definition it cannot begin with an opening curly brace (since it is indistinguishable from the code block) or a function
keyword (since it is indistinguishable from FD).
However in section of errors processing the standard allows for implementations extensions of program syntax. And one of such extensions can be seen in case of functions which appear in blocks. All implementations existing today do not throw an exception in this case and process it. But every in its own way.
Presence of if
-else
branches assumes a choice is being made which of the two function will be defined. Since this decision is to be made at runtime, that implies that a function expression (FE)should be used. However the majority of implementations will simply create both of the function declarations (FD) on entering the context stage, but since both of the functions use the same name, only the last declared function will get called. In this example the function foo
shows 1
although theelse
branch never executes.
However, SpiderMonkey implementation treats this case in two ways: on the one hand it does not consider such functions as declarations (i.e. the function is created on the condition at the code execution stage), but on the other hand they are not real function expressions since they cannot be called without surrounding parentheses (again the parse error — “indistinguishably from FD”) and they are stored in the variable object.
My opinion is that SpiderMonkey handles this case correctly, separating the own middle type of function — (FE + FD). Such functions are correctly created due the time and according to conditions, but also unlike FE, and more like FD, are available to be called from the outside. This syntactic extension SpiderMonkey names as Function Statement (in abbreviated form FS); this terminology ismentioned in MDC. JavaScript inventor Brendan Eich also noticed this type of functions provided by SpiderMonkey implementation.
這里有必要說明的是,按照標准,這種句法結構通常是不正確的,因為我們還記得,一個函數聲明(FD)不能出現在代碼塊中(這里if和else包含代碼塊)。我們曾經講過,FD僅出現在兩個位置:程序級(Program level)或直接位於其它函數體中。
因為代碼塊僅包含語句,所以這是不正確的。可以出現在塊中的函數的唯一位置是這些語句中的一個——上面已經討論過的表達式語句。但是,按照定義它不能以大括號開始(既然它有別於代碼塊)或以一個函數關鍵字開始(既然它有別於FD)。
但是,在標准的錯誤處理章節中,它允許程序語法的擴展執行。這樣的擴展之一就是我們見到的出現在代碼塊中的函數。在這個例子中,現今的所有存在的執行都不會拋出異常,都會處理它。但是它們都有自己的方式。
if-else分支語句的出現意味着一個動態的選擇。即,從邏輯上來說,它應該是在代碼執行階段動態創建的函數表達式(FE)。但是,大多數執行在進入上下文階段時簡單的創建函數聲明(FD),並使用最后聲明的函數。即,函數foo將顯示”1″,事實上else分支將永遠不會執行。
但是,SpiderMonkey (和TraceMonkey)以兩種方式對待這種情況:一方面它不會將函數作為聲明處理(即,函數在代碼執行階段根據條件創建),但另一方面,既然沒有括號包圍(再次出現解析錯誤——”與FD有別”),他們不能被調用,所以也不是真正的函數表達式,它儲存在變量對象中。
我個人認為這個例子中SpiderMonkey 的行為是正確的,拆分了它自身的函數中間類型——(FE+FD)。這些函數在合適的時間創建,根據條件,也不像FE,倒像一個可以從外部調用的FD,SpiderMonkey將這種語法擴展 稱之為函數語句(縮寫為FS);該語法在MDC中提及過。
命名函數表達式的特性
In case FE has a name (named function expression, in abbreviated form NFE) one important feature arises. As we know from definition (and as we saw in the examples above) function expressions do not influence variable object of a context (this means that it’s impossible to call them by namebefore or after their definition). However, FE can call itself by name in the recursive call:
當函數表達式FE有一個名稱(稱為命名函數表達式,縮寫為NFE)時,將會出現一個重要的特點。從定義(正如我們從上面示例中看到的那樣)中我們知道函數表達式不會影響一個上下文的變量對象(那樣意味着既不可能通過名稱在函數聲明之前調用它,也不可能在聲明之后調用它)。但是,FE在遞歸調用中可以通過名稱調用自身。
(
function
foo(bar) {
if
(bar) {
return
;
}
foo(
true
);
// "foo" name is available
})();
// but from the outside, correctly, is not
foo();
// "foo" is not defined
|
Where is the name “foo” stored? In the activation object of foo
? No, since nobody has defined any “foo” name inside foo
function. In the parent variable object of a context which creates foo
? Also not, remember the definition — FE does not influence the VO — what is exactly we see when callingfoo
from the outside. Where then?
Here’s how it works: when the interpreter at the code execution stage meets named FE, before creating FE, it creates auxiliary special object and adds it in front of the current scope chain. Then it creates FE itself at which stage the function gets the [[Scope]]
property (as we know from theChapter 4. Scope chain) — the scope chain of the context which created the function (i.e. in[[Scope]]
there is that special object). After that, the name of FE is added to the special object as unique property; value of this property is the reference to the FE. And the last action is removing that special object from the parent scope chain. Let’s see this algorithm on the pseudo-code:
foo”儲存在什么地方?在foo的活動對象中?不是,因為在foo中沒有定義任何”foo”。在上下文的父變量對象中創建foo?也不是,因為按照定義——FE不會影響VO(變量對象)——從外部調用foo我們可以實實在在的看到。那么在哪里呢?
以下是關鍵點。當解釋器在代碼執行階段遇到命名的FE時,在FE創建之前,它創建了輔助的特定對象,並添加到當前作用域鏈的最前端。然后它創建了FE,此時(正如我們在第四章 作用域鏈知道的那樣)函數獲取了[[Scope]] 屬性——創建這個函數上下文的作用域鏈)。此后,FE的名稱添加到特定對象上作為唯一的屬性;這個屬性的值是引用到FE上。最后一步是從父作用域鏈中移除那個特定的對象。讓我們在偽碼中看看這個算法:
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
|
Thus, from the outside this function name is not available (since it is not present in parent scope), but special object which has been saved in [[Scope]]
of a function and there this name is available.
It is necessary to note however, that some implementations, for example Rhino, save this optional name not in the special object but in the activation object of the FE. Implementation from Microsoft — JScript, completely breaking FE rules, keeps this name in the parent variables object and the function becomes available outside.
因此,在函數外部這個名稱不可用的(因為它不在父作用域鏈中),但是,特定對象已經存儲在函數的[[scope]]中,在那里名稱是可用的。
但是需要注意的是一些實現(如Rhino)不是在特定對象中而是在FE的激活對象中存儲這個可選的名稱。Microsoft 中的執行完全打破了FE規則,它在父變量對象中保持了這個名稱,這樣函數在外部變得可以訪問。
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.
說到實現,部分版本的SpiderMonkey有一個與上述提到的特殊對象相關的特性,這個特性也可以看作是個bug(既然所有的實現都是嚴格遵循標准的,那么這個就是標准的問題了)。 此特性和標識符處理相關: 作用域鏈的分析是二維的,在標識符查詢的時候,還要考慮作用域鏈中每個對象的原型鏈。
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:
當在Object.prototype對象上定義一個屬性,並將該屬性值指向一個“根本不存在”的變量時,就能夠體現該特性。 比如,如下例子中的變量“x”,在查詢過程中,通過作用域鏈,一直到全局對象也是找不到“x”的。 然而,在SpiderMonkey中,全局對象繼承自Object.prototype,於是,對應的值就在該對象中找到了:
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
:
活躍對象是沒有原型一說的。可以通過內部函數還證明。 如果在定義一個局部變量“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();
|
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
:
在有些實現中,存在這樣的異常:它們會在活躍對象設置原型。比方說,在Blackberry的實現中,上述例子中變量“x”值就會變成10。 因為,“x”從Object.prototype中就找到了:
AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
|
And we can see absolutely the same situation in SpiderMonkey in case of special object of a named FE. This special object (by the standard) is a normal object — “as if by expression new Object()
“, and accordingly it should be inherited from Object.prototype
, what is exactly what can be seen in SpiderMonkey implementation (but only up to version 1.7). Other implementations (including newer versions of SpiderMonkey) do not set a prototype for that special object:
當出現有名字的FE的特殊對象的時候,在SpiderMonkey中也是有同樣的異常。該特殊對象是常見對象 —— “和通過new Object()表達式產生的一樣”。 相應地,它也應當繼承自Object.prototype,上述描述只針對SpiderMonkey(1.7版本)。其他的實現(包括新的TraceMonkey)是不會給這個特殊對象設置原型的:
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:
微軟的實現——JScript,是IE的JS引擎(截至本文撰寫時最新是JScript5.8——IE8),該引擎與NFE相關的bug有很多。每個bug基本上都和ECMA-262-3rd標准是完全違背的。 有些甚至會引發嚴重的錯誤。
第一,針對上述這樣的情況,JScript完全破壞了FE的規則:不應當將函數名字保存在變量對象中的。 另外,FE的名字應當保存在特殊對象中,並且只有在函數自身內部才可以訪問(其他地方均不可以)。而JScript卻將其直接保存在上層上下文的變量對象中。 並且,JScript居然還將FE以FD的方式處理,在進入上下文的時候就將其創建出來,並在定義之前就可以訪問到:
// 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();
|
As we see, complete violation of rules.
正如大家所見,完全破壞了FE的規則。
Secondly, in case of assigning the named FE to a variable at declaration, JScript creates two different function objects. It is difficult to name such behavior as logical (especially considering that outside of NFE its name should not be accessible at all):
第二,在聲明同時,將NFE賦值給一個變量的時候,JScript會創建兩個不同的函數對象。 這種行為感覺完全不符合邏輯(特別是考慮到在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"
|
Again we see the full disorder.
However it is necessary to notice that if to describe NFE separately from assigning to variable (for example via the grouping operator), and only after that to assign it to a variable, then check on equality returns true
just like it would be one object:
然而,要注意的是: 當將NFE和賦值給變量這兩件事情分開的話(比如,通過組操作符),在定義好后,再進行變量賦值,這樣,兩個對象就相同了,返回true:
(
function
bar() {});
var
foo = bar;
alert(foo === bar);
// true
foo.x = 10;
alert(bar.x);
// 10
|
This moment can be explained. Actually, again two objects are created but after that remains, really, only one. If again to consider that NFE here is treated as the function declaration (FD) then on entering the context stage FD bar is created. After that, already at code execution stage the second object — function expression (FE) bar is created and is not saved anywhere. Accordingly, as there is no any reference on FE bar
it is removed. Thus there is only one object — FD bar
, the reference on which is assigned to foo
variable.
這個時候就好解釋了。實施上,一開始的確創建了兩個對象,不過之后就只剩下一個了。這里將NFE以FD的方式來處理,然后,當進入上下文的時候,FD bar就創建出來了。 在這之后,到了執行代碼階段,又創建出了第二個對象 —— FE bar,該對象不會進行保存。相應的,由於沒有變量對其進行引用,隨后FE bar對象就被移除了。 因此,這里就只剩下一個對象——FD bar對象,對該對象的引用就賦值給了foo變量。
Thirdly, regarding the indirect reference to a function via arguments.callee
, it references that object with which name a function is activated (to be exact — functions since there are two objects):
第三,通過arguments.callee對一個函數進行間接引用,它引用的是和激活函數名一致的對象(事實上是——函數,因為有兩個對象):
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會將NFE以FD來處理,但當遇到條件語句又不遵循此規則了。比如說,和FD那樣,NFE會在進入上下文的時候就創建出來,這樣最后一次定義的就會被使用:
var
foo =
function
bar() {
alert(1);
};
if
(
false
) {
foo =
function
bar() {
alert(2);
};
}
bar();
// 2
foo();
// 1
|
This behavior can also be “logically” explained. On entering the context stage the last met FD with name bar is created, i.e. function with alert(2)
. After that, at code execution stage already new function — FE bar
is created, the reference on which is assigned to foo
variable. Thus (as further in the code the if-block with a condition false
is unreachable), foo
activation produces alert(1)
. The logic is clear, but taking into account IE bugs, I have quoted “logically” word since such implementation is obviously broken and depends on JScript bugs.
And the fifth NFE bug in JScript is related with creation of properties of global object via assigning value to an unqualified identifier (i.e. without var
keyword). Since NFE is treated here as FD and, accordingly, stored in the variable object, assignment to unqualified identifier (i.e. not to variablebut to usual property of global object) in case when the function name is the same as unqualified identifier, this property does not become global.
上述行為從邏輯上也是可以解釋通的: 當進入上下文的時候,最后一次定義的FD bar被創建出來(有alert(2)的函數), 之后到了執行代碼階段又一個新的函數 —— FE bar被創建出來,對其引用賦值給了變量foo。因此(if代碼塊中由於判斷條件是false,因此其代碼塊中的代碼永遠不會被執行到)foo函數的調用會打印出1。 盡管“邏輯上”是對的,但是這個仍然算是IE的bug。因為它明顯就破壞了實現的規則,所以我這里用了引號“邏輯上”。
第五個JScript中NFE的bug和通過給一個未受限的標識符賦值(也就是說,沒有var關鍵字)來創建全局對象的屬性相關。 由於這里NFE會以FD的方式來處理,並相應地會保存在變量對象上,賦值給未受限的標識符(不是給變量而是給全局對象的一般屬性), 當函數名和標識符名字相同的時候,該屬性就不會是全局的了。
(
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 “logic” 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.
這里從“邏輯上”又是可以解釋通的: 進入上下文時,函數聲明在匿名函數本地上下文的活躍對象中。 當進入執行代碼階段的時候,因為foo這個名字已經在AO中存在了(本地),相應地,賦值操作也只是簡單的對AO中的foo進行更新而已。 並沒有在全局對象上創建新的屬性。
通過Function構造器創建的函數
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:
這類函數有別於FD和FE,有自己的專屬特性: 它們的[[Scope]]屬性中只包含全局對象:
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函數的[[Scope]]屬性並未包含foo上下文的AO —— 變量“y”是無法訪問的,並且變量“x”是來自全局上下文。 順便提下,這里要注意的是,Function構造器可以通過new關鍵字和省略new關鍵字兩種用法。上述例子中,這兩種用法都是一樣的。
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:
此類函數其他特性則和同類語法產生式以及聯合對象有關。 該機制在標准中建議在作優化的時候采用(當然,具體的實現者也完全有權利不使用這類優化)。比方說,有100元素的數組,在循環數組過程中會給數組每個元素賦值(函數), 這個時候,實現的時候就可以采用聯合對象的機制了。這樣,最終所有的數組元素都會引用同一個函數(只有一個函數):
var
a = [];
for
(
var
k = 0; k < 100; k++) {
a[k] =
function
() {};
// possibly, joined objects are used
}
|
But functions created via Function constructor are never joined:
但是,通過Function構造器創建的函數就無法使用聯合對象了:
var
a = [];
for
(
var
k = 0; k < 100; k++) {
a[k] =
Function
(
''
);
// always 100 different funcitons
}
|
Another example related with joined objects:
下面是另外一個和聯合對象相關的例子:
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.
上述例子,在實現過程中同樣可以使用聯合對象。來使得x和y引用同一個對象,因為函數(包括它們內部的[[Scope]]屬性)物理上是不可分辨的。 因此,通過Function構造器創建的函數總是會占用更多內存資源。
函數創建的算法
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.
如下所示使用偽代碼表示的函數創建的算法(不包含聯合對象的步驟)。有助於理解ECMAScript中的函數對象。此算法對所有函數類型都是一樣的。
F = new NativeObject(); // 屬性[[Class]] is "Function" F.[[Class]] = "Function" // 函數對象的原型 F.[[Prototype]] = Function.prototype // 對函數自身的引用 // [[Call]] is activated by call expression F() // 創建一個新的上下文 F.[[Call]] = <reference to function> // built in general constructor of objects 內置構造器 // [[Construct]] is activated via "new" keyword [[Construct]]是在new 關鍵字的時候激活。 // and it is the one who allocates memory for new 它會為新對象申請內存 // 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 當前上下文的作用域鏈 F.[[Scope]] = activeContext.Scope // if this functions is created // via new Function(...), then 如果是通過new 運算符來創建的,則 F.[[Scope]] = globalContext.Scope // number of formal parameters 形參的個數 F.length = countParameters // a prototype of created by F objects 通過F創建出來的原型 __objectPrototype = new Object(); __objectPrototype.constructor = F // {DontEnum}, is not enumerable in loops F.prototype = __objectPrototype return F
Pay attention, F.[[Prototype]] is a prototype of the function (constructor) and F.prototype is a prototype of objects created by this function (because often there is a mess in terminology, andF.prototype in some articles is named as a “prototype of the constructor” that is incorrect).
要注意的是,F.[[Prototype]]是函數(構造器)的原型,而F.prototype是通過該函數創建出來的對象的原型(因為通常對這兩個概念都會混淆,在有些文章中會將F.prototype叫做“構造器的原型”,這是錯誤的)。
結論
This article has turned out rather big; however, we will mention functions again when will discuss their work as constructors in one of chapters about objects and prototypes which follow. As always, I am glad to answer your questions in comments.
本文介紹了很多關於函數的內容;不過在后面的關於對象和原型的文章中,還會提到函數作為構造器是如何工作的。
附加閱讀
- 13. — Function Definition;
- 15.3 — Function Objects.