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

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

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

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

對象 Object

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

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

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

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/1.png" alt="" />

圖 1. 一個(gè)含有原型的基本對象

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

原型鏈(Prototype chain)

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

原型鏈?zhǔn)且粋€(gè)由對象組成的有限對象鏈由于實(shí)現(xiàn)繼承和共享屬性。

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

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

類似于類”A”,”B”,”C”,在ECMAScript中尼創(chuàng)建對象類”a”,”b”,”c”,相應(yīng)地, 對象“a” 擁有對象“b”和”c”的共同部分。同時(shí)對象“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]實(shí)現(xiàn)的。

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

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/2.png" alt="" />

圖 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è)角色:

// 構(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)建了一個(gè)特殊的屬性"constructor"
  // 指向a的構(gòu)造函數(shù)本身
  // 實(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)系:

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/3.png" alt="" />

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

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

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

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

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

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

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

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

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

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

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/4.png" alt="" />

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/5.png" alt="" />

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

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

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

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/6.png" alt="" />

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

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

變量對象(Variable Object)

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

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

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

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

var foo = 10;
function bar() {} // // 函數(shù)聲明
(function baz() {}); // 函數(shù)表達(dá)式
console.log(
  this.foo == foo, // true
  window.bar == bar // true
);
console.log(baz); // 引用錯(cuò)誤,baz沒有被定義

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/7.png" alt="" />

圖 7. 全局變量對象

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

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

活動對象(activation object)

當(dāng)函數(shù)被調(diào)用者激活,這個(gè)特殊的活動對象(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ù)表達(dá)式
}
foo(10, 20);

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/8.png" alt="" />

圖 8. 激活對象

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

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

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

作用域鏈(Scope Chains)

作用域鏈?zhǔn)且粋€(gè) 對象列表(list of objects) ,用以檢索上下文代碼中出現(xiàn)的標(biāo)識符(identifiers) 。

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

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

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

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

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

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/9.png" alt="" />

圖 9. 作用域鏈

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

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

我們再看下面這個(gè)例子:

Object.prototype.x = 10;
var w = 20;
var y = 30;
// 在SpiderMonkey全局對象里
// 例如,全局上下文的變量對象是從"Object.prototype"繼承到的
// 所以我們可以得到“沒有聲明的全局變量”
// 因?yàn)榭梢詮脑玩溨蝎@取
console.log(x); // 10
(function foo() {
  // "foo" 是局部變量
  var w = 40;
  var x = 100;
  // "x" 可以從"Object.prototype"得到,注意值是10哦
  // 因?yàn)閧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)ニ褜_parent__之前,首先會去__proto__的鏈接中。

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/10.png" alt="" />

圖 10. with 增大的作用域鏈

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

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

閉包(Closures)

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

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

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

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

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

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}
// "foo"返回的也是一個(gè)function
// 并且這個(gè)返回的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”的另一個(gè)類型就是自上而下[”downward funarg problem”].在這種情況下,父級的上下會存在,但是在判斷一個(gè)變量值的時(shí)候會有多義性。也就是,這個(gè)變量究竟應(yīng)該使用哪個(gè)作用域。是在函數(shù)創(chuàng)建時(shí)的作用域呢,還是在執(zhí)行時(shí)的作用域呢?為了避免這種多義性,可以采用閉包,也就是使用靜態(tài)作用域。

請看下面的例子:

// 全局變量 "x"
var x = 10;
// 全局function
function foo() {
  console.log(x);
}
(function (funArg) {
  // 局部變量 "x"
  var x = 20;
  // 這不會有歧義
  // 因?yàn)槲覀兪褂?foo"函數(shù)的[[Scope]]里保存的全局變量"x",
  // 并不是caller作用域的"x"
  funArg(); // 10, 而不是20
})(foo); // 將foo作為一個(gè)"funarg"傳遞下去

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

閉包是一系列代碼塊(在ECMAScript中是函數(shù)),并且靜態(tài)保存所有父級的作用域。通過這些保存的作用域來搜尋到函數(shù)中的自由變量。

請注意,因?yàn)槊恳粋€(gè)普通函數(shù)在創(chuàng)建時(shí)保存了[[Scope]],理論上,ECMAScript 中所有函數(shù)都是閉包。

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

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
);

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

http://wiki.jikexueyuan.com/project/javascript-depth-understanding/images/11.png" alt="" />

圖 11. 共享的[[Scope]]

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

var data = [];
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2

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

var data = [];
for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // 將k當(dāng)做參數(shù)傳遞進(jìn)去
} 
// 結(jié)果正確
data[0](); // 0
data[1](); // 1
data[2](); // 2

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

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

This 指針

this適合執(zhí)行的上下文環(huán)境息息相關(guān)的一個(gè)特殊對象。因此,它也可以稱為上下文對象[context object](激活執(zhí)行上下文的上下文)。

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

this是執(zhí)行上下文環(huán)境的一個(gè)屬性,而不是某個(gè)變量對象的屬性。

這個(gè)特點(diǎn)很重要,因?yàn)楹妥兞坎煌?,this 是沒有一個(gè)類似搜尋變量的過程。當(dāng)你在代碼中使用了 this,這個(gè) this 的值就直接從執(zhí)行的上下文中獲取了,而不會從作用域鏈中搜尋。this 的值只取決中進(jìn)入上下文時(shí)的情況。

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

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

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)用表達(dá)式[call expression]產(chǎn)生的(也就是這個(gè)函數(shù)如何被激活調(diào)用的)。例如,下面的例子中 foo 就是一個(gè) callee,在全局上下文中被激活。下面的例子就表明了不同的 caller 引起 this 的不同。

// "foo"函數(shù)里的alert沒有改變
// 但每次激活調(diào)用的時(shí)候this是不同的
function foo() {
  alert(this);
}
// caller 激活 "foo"這個(gè)callee,
// 并且提供"this"給這個(gè) callee
foo(); // 全局對象
foo.prototype.constructor(); // foo.prototype
var bar = {
  baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // 這是一個(gè)全局對象
(bar.baz, bar.baz)(); // 也是全局對象
(false || bar.baz)(); // 也是全局對象
var otherFoo = bar.baz;
otherFoo(); // 還是全局對象

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

總結(jié)(Conclusion)

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

同步與推薦

深入理解 JavaScript 系列文章,包括了原創(chuàng),翻譯,轉(zhuǎn)載等各類型的文章,如果對你有用,請推薦支持一把,給大叔寫作的動力。