深刻理解JavaScript內部原理: function(轉)

本文是翻譯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中一個很重要的對象-函數。咱們將詳細講解一下各類類型的函數是如何影響上下文的變量對象以及每一個函數的做用域鏈都包含什麼,咱們將回答諸如像下面這樣的問題:下面聲明的函數有什麼區別麼?(若是有,區別是什麼)。express

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?」:ecmascript

或者,下面的函數調用,爲何要用括號包圍起來。ide

(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.oop

函數類型
In ECMAScript there are three function types and each of them has its own features.優化

在ECMAScript中,有三種不一樣的函數類型,而且他們都有本身的特色。

函數聲明
A Function Declaration (abbreviated form is FD) is a function which:
函數聲明(簡寫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個位置能夠聲明函數,也就是說:不可能在表達式位置或一個代碼塊中定義它。

另一種能夠取代函數聲明的方式是函數表達式,解釋以下:

函數表達式
A Function Expression (abbreviated form is FE) is a function which:
函數表達式(簡寫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 _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(),而在函數內部(如遞歸調用),有可能使用名稱「_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() {};

表達式定義裏說明: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

至關一部分問題出現了,咱們爲何須要函數表達式?答案是很顯然的——在表達式中使用它們,」不會污染」變量對象。最簡單的例子是將一個函數做爲參數傳遞給其它函數。

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

在上述例子裏,FE賦值給了一個變量(也就是參數),函數將該表達式保存在內存中,並經過變量名來訪問(由於變量影響變量對象),以下:

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

另一個例子是建立封裝的閉包從外部上下文中隱藏輔助性數據(在下面的例子中咱們使用FE,它在建立後當即調用):

var foo = {};
 
(function initialize() {
 
  var x = 10;
 
  foo.bar = function () {
    alert(x);
  };
 
})();
 
foo.bar(); // 10;
 
alert(x); // "x" is not defined

咱們看到函數foo.bar(經過[[Scope]]屬性)訪問到函數initialize的內部變量「x」。同時,「x」在外部不能直接訪問。在許多庫中,這種策略經常使用來建立」私有」數據和隱藏輔助實體。在這種模式中,初始化的FE的名稱一般被忽略:

(function () {
 
  // initializing scope
 
})();

還有一個例子是:在代碼執行階段經過條件語句進行建立FE,不會污染變量對象VO。

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

關於圓括號的問題

讓咱們回頭並回答在文章開頭提到的問題——」爲什麼在函數建立後的當即調用中必須用圓括號來包圍它?」,答案就是:表達式句子的限制就是這樣的。

根據標準,表達式語句不能以一個大括號{開始是由於他很難與代碼塊區分,一樣,他也不能以函數關鍵字開始,由於很難與函數聲明進行區分。即,因此,若是咱們定義一個當即執行的函數,在其建立後當即按如下方式調用:

function () {
  ...
}();
 
// or even with a name
 
function foo() {
  ...
}();

咱們使用了函數聲明,上述2個定義,解釋器在解釋的時候都會報錯,可是可能有多種緣由。

若是在全局代碼裏定義(也就是程序級別),解釋器會將它看作是函數聲明,由於他是以function關鍵字開頭,第一個例子,咱們會獲得SyntaxError錯誤,是由於函數聲明沒有名字(咱們前面提到了函數聲明必須有名字)。

第二個例子,咱們有一個名稱爲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

上述代碼是沒有問題的,由於聲明的時候產生了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");

根據規範,上述代碼是錯誤的(一個表達式語句不能以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.

咱們若是來告訴解釋器:我就像在函數聲明以後當即調用,答案是很明確的,你得聲明函數表達式function expression,而不是函數聲明function declaration,而且建立表達式最簡單的方式就是用分組操做符括號,裏邊放入的永遠是表達式,因此解釋器在解釋的時候就不會出現歧義。在代碼執行階段這個的function就會被建立,而且當即執行,而後自動銷燬(若是沒有引用的話)。

(function foo(x) {
  alert(x);
})(1); // OK, it's a call, not a grouping operator, 1

上述代碼就是咱們所說的在用括號括住一個表達式,而後經過(1)去調用。

注意,下面一個當即執行的函數,周圍的括號不是必須的,由於函數已經處在表達式的位置,解析器知道它處理的是在函數執行階段應該被建立的FE,這樣在函數建立後當即調用了函數。

var foo = {
 
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
 
};
 
alert(foo.bar); // 'yes'

就像咱們看到的,foo.bar是一個字符串而不是一個函數,這裏的函數僅僅用來根據條件參數初始化這個屬性——它建立後並當即調用。

所以,」關於圓括號」問題完整的答案以下:當函數不在表達式的位置的時候,分組操做符圓括號是必須的——也就是手工將函數轉化成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

...

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

順便提一句,組表達式包圍函數描述能夠沒有調用圓括號,也可包含調用圓括號,即,下面的兩個表達式都是正確的FE。

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

實現擴展: 函數語句

下面的代碼,根據貴方任何一個function聲明都不該該被執行:

if (true) {
 
  function foo() {
    alert(0);
  }
 
} else {
 
  function foo() {
    alert(1);
  }
 
}
 
foo(); // 1 or 0 ? test in different implementations

這裏有必要說明的是,按照標準,這種句法結構一般是不正確的,由於咱們還記得,一個函數聲明(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中說起過。

命名函數表達式的特性

當函數表達式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

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

所以,在函數外部這個名稱不可用的(由於它不在父做用域鏈中),可是,特定對象已經存儲在函數的[[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

當出現有名字的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();

正如你們所見,徹底破壞了FE的規則。

第二,在聲明同時,將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"

然而,要注意的是: 當將NFE和賦值給變量這兩件事情分開的話(好比,經過組操做符),在定義好後,再進行變量賦值,這樣,兩個對象就相同了,返回true:

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

這個時候就好解釋了。實施上,一開始的確建立了兩個對象,不過以後就只剩下一個了。這裏將NFE以FD的方式來處理,而後,當進入上下文的時候,FD bar就建立出來了。 在這以後,到了執行代碼階段,又建立出了第二個對象 —— FE bar,該對象不會進行保存。相應的,因爲沒有變量對其進行引用,隨後FE bar對象就被移除了。 所以,這裏就只剩下一個對象——FD bar對象,對該對象的引用就賦值給了foo變量。

第三,經過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

上述行爲從邏輯上也是能夠解釋通的: 當進入上下文的時候,最後一次定義的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
}

可是,經過Function構造器建立的函數就沒法使用聯合對象了:

var a = [];
 
for (var k = 0; k < 100; k++) {
  a[k] = Function(''); // always 100 different funcitons
}

下面是另一個和聯合對象相關的例子:

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

要注意的是,F.[[Prototype]]是函數(構造器)的原型,而F.prototype是經過該函數建立出來的對象的原型(由於一般對這兩個概念都會混淆,在有些文章中會將F.prototype叫作「構造器的原型」,這是錯誤的)。

結論

本文介紹了不少關於函數的內容;不過在後面的關於對象和原型的文章中,還會提到函數做爲構造器是如何工做的。

相關文章
相關標籤/搜索