JavaScript核心(晉級高手必讀篇)

2018-09-25 10:43 更新

本篇是ECMA-262-3 in detail系列的一個概述(本人后續(xù)會翻譯整理這些文章到本系列(第11-19章)。每個章節(jié)都有一個更詳細(xì)的內(nèi)容鏈接,你可以繼續(xù)讀一下每個章節(jié)對應(yīng)的詳細(xì)內(nèi)容鏈接進行更深入的了解。

適合的讀者:有經(jīng)驗的開發(fā)員,專業(yè)前端人員。

原作者: Dmitry A. Soshnikov
發(fā)布時間: 2010-09-02
原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
參考1:http://ued.ctrip.com/blog/?p=2795
參考2:http://www.cnblogs.com/ifishing/archive/2010/12/08/1900594.html
主要是綜合了上面2位高手的中文翻譯,將兩篇文章的精華部分都結(jié)合在一起了。

我們首先來看一下對象[Object]的概念,這也是ECMASript中最基本的概念。

對象Object

ECMAScript是一門高度抽象的面向?qū)ο?object-oriented)語言,用以處理Objects對象. 當(dāng)然,也有基本類型,但是必要時,也需要轉(zhuǎn)換成object對象來用。

An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value.

Object是一個屬性的集合,并且都擁有一個單獨的原型對象[prototype object]. 這個原型對象[prototype object]可以是一個object或者null值。

讓我們來舉一個基本Object的例子,首先我們要清楚,一個Object的prototype是一個內(nèi)部的[[prototype]]屬性的引用。

不過一般來說,我們會使用<內(nèi)部屬性名> 下劃線來代替雙括號,例如proto(這是某些腳本引擎比如SpiderMonkey的對于原型概念的具體實現(xiàn),盡管并非標(biāo)準(zhǔn))。

var foo = {
  x: 10,
  y: 20
};

上述代碼foo對象有兩個顯式的屬性[explicit own properties]和一個自帶隱式的 proto 屬性[implicit proto property],指向foo的原型。

圖 1. 一個含有原型的基本對象

為什么需要原型呢,讓我們考慮 原型鏈 的概念來回答這個問題。

原型鏈(Prototype chain)

原型對象也是普通的對象,并且也有可能有自己的原型,如果一個原型對象的原型不為null的話,我們就稱之為原型鏈(prototype chain)。

A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties.
原型鏈?zhǔn)且粋€由對象組成的有限對象鏈由于實現(xiàn)繼承和共享屬性。

想象一個這種情況,2個對象,大部分內(nèi)容都一樣,只有一小部分不一樣,很明顯,在一個好的設(shè)計模式中,我們會需要重用那部分相同的,而不是在每個對象中重復(fù)定義那些相同的方法或者屬性。在基于類[class-based]的系統(tǒng)中,這些重用部分被稱為類的繼承 – 相同的部分放入class A,然后class B和class C從A繼承,并且可以聲明擁有各自的獨特的東西。

ECMAScript沒有類的概念。但是,重用[reuse]這個理念沒什么不同(某些方面,甚至比class-更加靈活),可以由prototype chain原型鏈來實現(xiàn)。這種繼承被稱為delegation based inheritance-基于繼承的委托,或者更通俗一些,叫做原型繼承。

類似于類”A”,”B”,”C”,在ECMAScript中尼創(chuàng)建對象類”a”,”b”,”c”,相應(yīng)地, 對象“a” 擁有對象“b”和”c”的共同部分。同時對象“b”和”c”只包含它們自己的附加屬性或方法。

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  proto: a
};

var c = {
  y: 30,
  proto: a
};

// 調(diào)用繼承過來的方法
b.calculate(30); // 60
c.calculate(40); // 80

這樣看上去是不是很簡單啦。b和c可以使用a中定義的calculate方法,這就是有原型鏈來[prototype chain]實現(xiàn)的。

原理很簡單:如果在對象b中找不到calculate方法(也就是對象b中沒有這個calculate屬性), 那么就會沿著原型鏈開始找。如果這個calculate方法在b的prototype中沒有找到,那么就會沿著原型鏈找到a的prototype,一直遍歷完整個原型鏈。記住,一旦找到,就返回第一個找到的屬性或者方法。因此,第一個找到的屬性成為繼承屬性。如果遍歷完整個原型鏈,仍然沒有找到,那么就會返回undefined。

注意一點,this這個值在一個繼承機制中,仍然是指向它原本屬于的對象,而不是從原型鏈上找到它時,它所屬于的對象。例如,以上的例子,this.y是從b和c中獲取的,而不是a。當(dāng)然,你也發(fā)現(xiàn)了this.x是從a取的,因為是通過原型鏈機制找到的。

如果一個對象的prototype沒有顯示的聲明過或定義過,那么prototype的默認(rèn)值就是object.prototype, 而object.prototype也會有一個prototype, 這個就是原型鏈的終點了,被設(shè)置為null。

下面的圖示就是表示了上述a,b,c的繼承關(guān)系

圖 2. 原型鏈

原型鏈通常將會在這樣的情況下使用:對象擁有 相同或相似的狀態(tài)結(jié)構(gòu)(same or similar state structure) (即相同的屬性集合)與 不同的狀態(tài)值(different state values)。在這種情況下,我們可以使用 構(gòu)造函數(shù)(Constructor) 在 特定模式(specified pattern) 下創(chuàng)建對象。

構(gòu)造函數(shù)(Constructor)

除了創(chuàng)建對象,構(gòu)造函數(shù)(constructor) 還做了另一件有用的事情—自動為創(chuàng)建的新對象設(shè)置了原型對象(prototype object) 。原型對象存放于 ConstructorFunction.prototype 屬性中。

例如,我們重寫之前例子,使用構(gòu)造函數(shù)創(chuàng)建對象“b”和“c”,那么對象”a”則扮演了“Foo.prototype”這個角色:

// 構(gòu)造函數(shù)
function Foo(y) {
  // 構(gòu)造函數(shù)將會以特定模式創(chuàng)建對象:被創(chuàng)建的對象都會有"y"屬性
  this.y = y;
}

// "Foo.prototype"存放了新建對象的原型引用
// 所以我們可以將之用于定義繼承和共享屬性或方法
// 所以,和上例一樣,我們有了如下代碼:

// 繼承屬性"x"
Foo.prototype.x = 10;

// 繼承方法"calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// 使用foo模式創(chuàng)建 "b" and "c"
var b = new Foo(20);
var c = new Foo(30);

// 調(diào)用繼承的方法
b.calculate(30); // 60
c.calculate(40); // 80

// 讓我們看看是否使用了預(yù)期的屬性

console.log(

  b.proto === Foo.prototype, // true
  c.proto === Foo.prototype, // true

  // "Foo.prototype"自動創(chuàng)建了一個特殊的屬性"constructor"
  // 指向a的構(gòu)造函數(shù)本身
  // 實例"b"和"c"可以通過授權(quán)找到它并用以檢測自己的構(gòu)造函數(shù)

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.proto.calculate, // true
  b.proto.calculate === Foo.prototype.calculate // true

);

上述代碼可表示為如下的關(guān)系:

圖 3. 構(gòu)造函數(shù)與對象之間的關(guān)系

上述圖示可以看出,每一個object都有一個prototype. 構(gòu)造函數(shù)Foo也擁有自己的proto, 也就是Function.prototype, 而Function.prototype的proto指向了Object.prototype. 重申一遍,F(xiàn)oo.prototype只是一個顯式的屬性,也就是b和c的proto屬性。

這個問題完整和詳細(xì)的解釋可以在大叔即將翻譯的第18、19兩章找到。有兩個部分:面向?qū)ο缶幊?一般理論(OOP. The general theory),描述了不同的面向?qū)ο蟮姆妒脚c風(fēng)格(OOP paradigms and stylistics),以及與ECMAScript的比較, 面向?qū)ο缶幊?ECMAScript實現(xiàn)(OOP. ECMAScript implementation), 專門講述了ECMAScript中的面向?qū)ο缶幊獭?/p>

現(xiàn)在,我們已經(jīng)了解了基本的object原理,那么我們接下去來看看ECMAScript里面的程序執(zhí)行環(huán)境[runtime program execution]. 這就是通常稱為的“執(zhí)行上下文堆?!盵execution context stack]。每一個元素都可以抽象的理解為object。你也許發(fā)現(xiàn)了,沒錯,在ECMAScript中,幾乎處處都能看到object的身影。

執(zhí)行上下文棧(Execution Context Stack)

在ECMASscript中的代碼有三種類型:global, function和eval。

每一種代碼的執(zhí)行都需要依賴自身的上下文。當(dāng)然global的上下文可能涵蓋了很多的function和eval的實例。函數(shù)的每一次調(diào)用,都會進入函數(shù)執(zhí)行中的上下文,并且來計算函數(shù)中變量等的值。eval函數(shù)的每一次執(zhí)行,也會進入eval執(zhí)行中的上下文,判斷應(yīng)該從何處獲取變量的值。

注意,一個function可能產(chǎn)生無限的上下文環(huán)境,因為一個函數(shù)的調(diào)用(甚至遞歸)都產(chǎn)生了一個新的上下文環(huán)境。

function foo(bar) {}

// 調(diào)用相同的function,每次都會產(chǎn)生3個不同的上下文
//(包含不同的狀態(tài),例如參數(shù)bar的值)

foo(10);
foo(20);
foo(30);

一個執(zhí)行上下文可以激活另一個上下文,就好比一個函數(shù)調(diào)用了另一個函數(shù)(或者全局的上下文調(diào)用了一個全局函數(shù)),然后一層一層調(diào)用下去。邏輯上來說,這種實現(xiàn)方式是棧,我們可以稱之為上下文堆棧。

激活其它上下文的某個上下文被稱為 調(diào)用者(caller) 。被激活的上下文被稱為被調(diào)用者(callee) 。被調(diào)用者同時也可能是調(diào)用者(比如一個在全局上下文中被調(diào)用的函數(shù)調(diào)用某些自身的內(nèi)部方法)。

當(dāng)一個caller激活了一個callee,那么這個caller就會暫停它自身的執(zhí)行,然后將控制權(quán)交給這個callee. 于是這個callee被放入堆棧,稱為進行中的上下文[running/active execution context]. 當(dāng)這個callee的上下文結(jié)束之后,會把控制權(quán)再次交給它的caller,然后caller會在剛才暫停的地方繼續(xù)執(zhí)行。在這個caller結(jié)束之后,會繼續(xù)觸發(fā)其他的上下文。一個callee可以用返回(return)或者拋出異常(exception)來結(jié)束自身的上下文。

如下圖,所有的ECMAScript的程序執(zhí)行都可以看做是一個執(zhí)行上下文堆棧[execution context (EC) stack]。堆棧的頂部就是處于激活狀態(tài)的上下文。

圖 4. 執(zhí)行上下文棧

當(dāng)一段程序開始時,會先進入全局執(zhí)行上下文環(huán)境[global execution context], 這個也是堆棧中最底部的元素。此全局程序會開始初始化,初始化生成必要的對象[objects]和函數(shù)[functions]. 在此全局上下文執(zhí)行的過程中,它可能會激活一些方法(當(dāng)然是已經(jīng)初始化過的),然后進入他們的上下文環(huán)境,然后將新的元素壓入堆棧。在這些初始化都結(jié)束之后,這個系統(tǒng)會等待一些事件(例如用戶的鼠標(biāo)點擊等),會觸發(fā)一些方法,然后進入一個新的上下文環(huán)境。

見圖5,有一個函數(shù)上下文“EC1″和一個全局上下文“Global EC”,下圖展現(xiàn)了從“Global EC”進入和退出“EC1″時棧的變化:

 圖 5. 執(zhí)行上下文棧的變化

ECMAScript運行時系統(tǒng)就是這樣管理代碼的執(zhí)行。

關(guān)于ECMAScript執(zhí)行上下文棧的內(nèi)容請查閱本系列教程的第11章執(zhí)行上下文(Execution context)。

如上所述,棧中每一個執(zhí)行上下文可以表示為一個對象。讓我們看看上下文對象的結(jié)構(gòu)以及執(zhí)行其代碼所需的 狀態(tài)(state) 。

執(zhí)行上下文(Execution Context)

一個執(zhí)行的上下文可以抽象的理解為object。每一個執(zhí)行的上下文都有一系列的屬性(我們稱為上下文狀態(tài)),他們用來追蹤關(guān)聯(lián)代碼的執(zhí)行進度。這個圖示就是一個context的結(jié)構(gòu)。

 圖 6. 上下文結(jié)構(gòu)

除了這3個所需要的屬性(變量對象(variable object),this指針(this value),作用域鏈(scope chain) ),執(zhí)行上下文根據(jù)具體實現(xiàn)還可以具有任意額外屬性。接著,讓我們仔細(xì)來看看這三個屬性。

變量對象(Variable Object)

A variable object is a scope of data related with the execution context. 
It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

變量對象(variable object) 是與執(zhí)行上下文相關(guān)的 數(shù)據(jù)作用域(scope of data) 。
它是與上下文關(guān)聯(lián)的特殊對象,用于存儲被定義在上下文中的 變量(variables) 和 函數(shù)聲明(function declarations) 。

注意:函數(shù)表達式[function expression](而不是函數(shù)聲明[function declarations,區(qū)別請參考本系列第2章])是不包含在VO[variable object]里面的。

變量對象(Variable Object)是一個抽象的概念,不同的上下文中,它表示使用不同的object。例如,在global全局上下文中,變量對象也是全局對象自身[global object]。(這就是我們可以通過全局對象的屬性來指向全局變量)。

讓我們看看下面例子中的全局執(zhí)行上下文情況:

var foo = 10;

function bar() {} // // 函數(shù)聲明
(function baz() {}); // 函數(shù)表達式

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // 引用錯誤,baz沒有被定義

全局上下文中的變量對象(VO)會有如下屬性:

圖 7. 全局變量對象

如上所示,函數(shù)“baz”如果作為函數(shù)表達式則不被不被包含于變量對象。這就是在函數(shù)外部嘗試訪問產(chǎn)生引用錯誤(ReferenceError) 的原因。請注意,ECMAScript和其他語言相比(比如C/C++),僅有函數(shù)能夠創(chuàng)建新的作用域。在函數(shù)內(nèi)部定義的變量與內(nèi)部函數(shù),在外部非直接可見并且不污染全局對象。使用 eval 的時候,我們同樣會使用一個新的(eval創(chuàng)建)執(zhí)行上下文。eval會使用全局變量對象或調(diào)用者的變量對象(eval的調(diào)用來源)。

那函數(shù)以及自身的變量對象又是怎樣的呢?在一個函數(shù)上下文中,變量對象被表示為活動對象(activation object)。

活動對象(activation object)

當(dāng)函數(shù)被調(diào)用者激活,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)?;顒訉ο笤诤瘮?shù)上下文中作為變量對象使用。

即:函數(shù)的變量對象保持不變,但除去存儲變量與函數(shù)聲明之外,還包含以及特殊對象arguments 。

考慮下面的情況:

function foo(x, y) {
  var z = 30;
  function bar() {} // 函數(shù)聲明
  (function baz() {}); // 函數(shù)表達式
}

foo(10, 20);

“foo”函數(shù)上下文的下一個激活對象(AO)如下圖所示:

圖 8. 激活對象

同樣道理,function expression不在AO的行列。

對于這個AO的詳細(xì)內(nèi)容可以通過本系列教程第9章找到。

我們接下去要講到的是第三個主要對象。眾所周知,在ECMAScript中,我們會用到內(nèi)部函數(shù)[inner functions],在這些內(nèi)部函數(shù)中,我們可能會引用它的父函數(shù)變量,或者全局的變量。我們把這些變量對象成為上下文作用域?qū)ο骩scope object of the context]. 類似于上面討論的原型鏈[prototype chain],我們在這里稱為作用域鏈[scope chain]。

作用域鏈(Scope Chains)

A scope chain is a list of objects that are searched for identifiers appear in the code of the context.
作用域鏈?zhǔn)且粋€ 對象列表(list of objects) ,用以檢索上下文代碼中出現(xiàn)的 標(biāo)識符(identifiers) 。

作用域鏈的原理和原型鏈很類似,如果這個變量在自己的作用域中沒有,那么它會尋找父級的,直到最頂層。

標(biāo)示符[Identifiers]可以理解為變量名稱、函數(shù)聲明和普通參數(shù)。例如,當(dāng)一個函數(shù)在自身函數(shù)體內(nèi)需要引用一個變量,但是這個變量并沒有在函數(shù)內(nèi)部聲明(或者也不是某個參數(shù)名),那么這個變量就可以稱為自由變量[free variable]。那么我們搜尋這些自由變量就需要用到作用域鏈。

在一般情況下,一個作用域鏈包括父級變量對象(variable object)(作用域鏈的頂部)、函數(shù)自身變量VO和活動對象(activation object)。不過,有些情況下也會包含其它的對象,例如在執(zhí)行期間,動態(tài)加入作用域鏈中的—例如with或者catch語句。[譯注:with-objects指的是with語句,產(chǎn)生的臨時作用域?qū)ο螅籧atch-clauses指的是catch從句,如catch(e),這會產(chǎn)生異常對象,導(dǎo)致作用域變更]。

當(dāng)查找標(biāo)識符的時候,會從作用域鏈的活動對象部分開始查找,然后(如果標(biāo)識符沒有在活動對象中找到)查找作用域鏈的頂部,循環(huán)往復(fù),就像作用域鏈那樣。

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x"和"y"是自由變量
    // 會在作用域鏈的下一個對象中找到(函數(shù)”bar”的互動對象之后)
    console.log(x + y + z);
  })();
})();

我們假設(shè)作用域鏈的對象聯(lián)動是通過一個叫做parent的屬性,它是指向作用域鏈的下一個對象。這可以在Rhino Code中測試一下這種流程,這種技術(shù)也確實在ES5環(huán)境中實現(xiàn)了(有一個稱為outer鏈接).當(dāng)然也可以用一個簡單的數(shù)據(jù)來模擬這個模型。使用parent的概念,我們可以把上面的代碼演示成如下的情況。(因此,父級變量是被存在函數(shù)的[[Scope]]屬性中的)。

圖 9. 作用域鏈

在代碼執(zhí)行過程中,如果使用with或者catch語句就會改變作用域鏈。而這些對象都是一些簡單對象,他們也會有原型鏈。這樣的話,作用域鏈會從兩個維度來搜尋。

  1.     首先在原本的作用域鏈
  2.     每一個鏈接點的作用域的鏈(如果這個鏈接點是有prototype的話)

我們再看下面這個例子:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// 在SpiderMonkey全局對象里
// 例如,全局上下文的變量對象是從"Object.prototype"繼承到的
// 所以我們可以得到“沒有聲明的全局變量”
// 因為可以從原型鏈中獲取

console.log(x); // 10

(function foo() {

  // "foo" 是局部變量
  var w = 40;
  var x = 100;

  // "x" 可以從"Object.prototype"得到,注意值是10哦
  // 因為{z: 50}是從它那里繼承的

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // 在"with"對象從作用域鏈刪除之后
  // x又可以從foo的上下文中得到了,注意這次值又回到了100哦
  // "w" 也是局部變量
  console.log(x, w); // 100, 40

  // 在瀏覽器里
  // 我們可以通過如下語句來得到全局的w值
  console.log(window.w); // 20

})();

我們就會有如下結(jié)構(gòu)圖示。這表示,在我們?nèi)ニ褜?strong>parent之前,首先會去proto的鏈接中。

圖 10. with增大的作用域鏈

注意,不是所有的全局對象都是由Object.prototype繼承而來的。上述圖示的情況可以在SpiderMonkey中測試。

只要所有外部函數(shù)的變量對象都存在,那么從內(nèi)部函數(shù)引用外部數(shù)據(jù)則沒有特別之處——我們只要遍歷作用域鏈表,查找所需變量。然而,如上文所提及,當(dāng)一個上下文終止之后,其狀態(tài)與自身將會被 銷毀(destroyed) ,同時內(nèi)部函數(shù)將會從外部函數(shù)中返回。此外,這個返回的函數(shù)之后可能會在其他的上下文中被激活,那么如果一個之前被終止的含有一些自由變量的上下文又被激活將會怎樣?通常來說,解決這個問題的概念在ECMAScript中與作用域鏈直接相關(guān),被稱為 (詞法)閉包((lexical) closure)。

閉包(Closures)

在ECMAScript中,函數(shù)是“第一類”對象。這個名詞意味著函數(shù)可以作為參數(shù)被傳遞給其他函數(shù)使用 (在這種情況下,函數(shù)被稱為“funargs”——“functional arguments”的縮寫[譯注:這里不知翻譯為泛函參數(shù)是否恰當(dāng)])。接收“funargs”的函數(shù)被稱之為 高階函數(shù)(higher-order functions) ,或者更接近數(shù)學(xué)概念的話,被稱為 運算符(operators) 。其他函數(shù)的運行時也會返回函數(shù),這些返回的函數(shù)被稱為 function valued 函數(shù) (有 functional value 的函數(shù))。

“funargs”與“functional values”有兩個概念上的問題,這兩個子問題被稱為“Funarg problem” (“泛函參數(shù)問題”)。要準(zhǔn)確解決泛函參數(shù)問題,需要引入 閉包(closures) 到的概念。讓我們仔細(xì)描述這兩個問題(我們可以見到,在ECMAScript中使用了函數(shù)的[[Scope]]屬性來解決這個問題)。

“funarg problem”的一個子問題是“upward funarg problem”[譯注:或許可以翻譯為:向上查找的函數(shù)參數(shù)問題]。當(dāng)一個函數(shù)從其他函數(shù)返回到外部的時候,這個問題將會出現(xiàn)。要能夠在外部上下文結(jié)束時,進入外部上下文的變量,內(nèi)部函數(shù) 在創(chuàng)建的時候(at creation moment) 需要將之存儲進[[Scope]]屬性的父元素的作用域中。然后當(dāng)函數(shù)被激活時,上下文的作用域鏈表現(xiàn)為激活對象與[[Scope]]屬性的組合(事實上,可以在上圖見到):

Scope chain = Activation object + [[Scope]]
作用域鏈 = 活動對象 + [[Scope]]

請注意,最主要的事情是——函數(shù)在被創(chuàng)建時保存外部作用域,是因為這個 被保存的作用域鏈(saved scope chain) 將會在未來的函數(shù)調(diào)用中用于變量查找。

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo"返回的也是一個function
// 并且這個返回的function可以隨意使用內(nèi)部的變量x

var returnedFunction = foo();

// 全局變量 "x"
var x = 20;

// 支持返回的function
returnedFunction(); // 結(jié)果是10而不是20

這種形式的作用域稱為靜態(tài)作用域[static/lexical scope]。上面的x變量就是在函數(shù)bar的[[Scope]]中搜尋到的。理論上來說,也會有動態(tài)作用域[dynamic scope], 也就是上述的x被解釋為20,而不是10. 但是EMCAScript不使用動態(tài)作用域。

“funarg problem”的另一個類型就是自上而下[”downward funarg problem”].在這種情況下,父級的上下會存在,但是在判斷一個變量值的時候會有多義性。也就是,這個變量究竟應(yīng)該使用哪個作用域。是在函數(shù)創(chuàng)建時的作用域呢,還是在執(zhí)行時的作用域呢?為了避免這種多義性,可以采用閉包,也就是使用靜態(tài)作用域。

請看下面的例子:

 

// 全局變量 "x"
var x = 10;

// 全局function
function foo() {
  console.log(x);
}

(function (funArg) {

  // 局部變量 "x"
  var x = 20;

  // 這不會有歧義
  // 因為我們使用"foo"函數(shù)的[[Scope]]里保存的全局變量"x",
  // 并不是caller作用域的"x"

  funArg(); // 10, 而不是20

})(foo); // 將foo作為一個"funarg"傳遞下去

從上述的情況,我們似乎可以斷定,在語言中,使用靜態(tài)作用域是閉包的一個強制性要求。不過,在某些語言中,會提供動態(tài)和靜態(tài)作用域的結(jié)合,可以允許開發(fā)員選擇哪一種作用域。但是在ECMAScript中,只采用了靜態(tài)作用域。所以ECMAScript完全支持使用[[Scope]]的屬性。我們可以給閉包得出如下定義:

A closure is a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes.
Thus, via these saved scopes a function may easily refer free variables.
閉包是一系列代碼塊(在ECMAScript中是函數(shù)),并且靜態(tài)保存所有父級的作用域。通過這些保存的作用域來搜尋到函數(shù)中的自由變量。

請注意,因為每一個普通函數(shù)在創(chuàng)建時保存了[[Scope]],理論上,ECMAScript中所有函數(shù)都是閉包。

還有一個很重要的點,幾個函數(shù)可能含有相同的父級作用域(這是一個很普遍的情況,例如有好幾個內(nèi)部或者全局的函數(shù))。在這種情況下,在[[Scope]]中存在的變量是會共享的。一個閉包中變量的變化,也會影響另一個閉包的。

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

上述代碼可以用這張圖來表示:

圖 11. 共享的[[Scope]]

在某個循環(huán)中創(chuàng)建多個函數(shù)時,上圖會引發(fā)一個困惑。如果在創(chuàng)建的函數(shù)中使用循環(huán)變量(如”k”),那么所有的函數(shù)都使用同樣的循環(huán)變量,導(dǎo)致一些程序員經(jīng)常會得不到預(yù)期值。現(xiàn)在清楚為什么會產(chǎn)生如此問題了——因為所有函數(shù)共享同一個[[Scope]],其中循環(huán)變量為最后一次復(fù)賦值。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data0; // 3, but not 0
data1; // 3, but not 1
data2; // 3, but not 2

有一些用以解決這類問題的技術(shù)。其中一種技巧是在作用域鏈中提供一個額外的對象,比如增加一個函數(shù):

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // 將k當(dāng)做參數(shù)傳遞進去
}

// 結(jié)果正確
data0; // 0
data1; // 1
data2; // 2

閉包理論的深入研究與具體實踐可以在本系列教程第16章閉包(Closures)中找到。如果想得到關(guān)于作用域鏈的更多信息,可以參照本系列教程第14章作用域鏈(Scope chain)。

下一章節(jié)將會討論一個執(zhí)行上下文的最后一個屬性——this指針的概念。

This指針

A this value is a special object which is related with the execution context. 
Therefore, it may be named as a context object (i.e. an object in which context the execution context is activated).
this適合執(zhí)行的上下文環(huán)境息息相關(guān)的一個特殊對象。因此,它也可以稱為上下文對象context object。

任何對象都可以作為上下文的this值。我想再次澄清對與ECMAScript中,與執(zhí)行上下文相關(guān)的一些描述——特別是this的誤解。通常,this 被錯誤地,描述為變量對象的屬性。最近比如在這本書中就發(fā)現(xiàn)了(盡管書中提及this的那一章還不錯)。 請牢記:

a this value is a property of the execution context, but not a property of the variable object.
this是執(zhí)行上下文環(huán)境的一個屬性,而不是某個變量對象的屬性

這個特點很重要,因為和變量不同,this是沒有一個類似搜尋變量的過程。當(dāng)你在代碼中使用了this,這個 this的值就直接從執(zhí)行的上下文中獲取了,而不會從作用域鏈中搜尋。this的值只取決中進入上下文時的情況。

順便說一句,和ECMAScript不同,Python有一個self的參數(shù),和this的情況差不多,但是可以在執(zhí)行過程中被改變。在ECMAScript中,是不可以給this賦值的,因為,還是那句話,this不是變量。

在global context(全局上下文)中,this的值就是指全局這個對象,這就意味著,this值就是這個變量本身。

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

在函數(shù)上下文[function context]中,this會可能會根據(jù)每次的函數(shù)調(diào)用而成為不同的值.this會由每一次caller提供,caller是通過調(diào)用表達式[call expression]產(chǎn)生的(也就是這個函數(shù)如何被激活調(diào)用的)。例如,下面的例子中foo就是一個callee,在全局上下文中被激活。下面的例子就表明了不同的caller引起this的不同。

// "foo"函數(shù)里的alert沒有改變
// 但每次激活調(diào)用的時候this是不同的

function foo() {
  alert(this);
}

// caller 激活 "foo"這個callee,
// 并且提供"this"給這個 callee

foo(); // 全局對象
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // 這是一個全局對象
(bar.baz, bar.baz)(); // 也是全局對象
(false || bar.baz)(); // 也是全局對象

var otherFoo = bar.baz;
otherFoo(); // 還是全局對象

如果要深入思考每一次函數(shù)調(diào)用中,this值的變化(更重要的是怎樣變化),你可以閱讀本系列教程第10章This。上文所提及的情況都會在此章內(nèi)詳細(xì)討論。

總結(jié)(Conclusion)

在此我們完成了一個簡短的概述。盡管看來不是那么簡短,但是這些話題若要完整表述完畢,則需要一整本書。.我們沒有提及兩個重要話題:函數(shù)(functions) (以及不同類型的函數(shù)之間的不同,比如函數(shù)聲明與函數(shù)表達式)與ECMAScript的 求值策略(evaluation strategy) 。這兩個話題可以分別查閱本系列教程第15章函數(shù)(Functions) 與第19章求值策略(Evaluation strategy)。

如果你有任何評論,問題或者補充,我很歡迎在文章評論中討論。

祝大家學(xué)習(xí)ECMAScript順利。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號