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

鍍金池/ 教程/ HTML/ JavaScript 核心(晉級(jí)高手必讀篇)
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計(jì)模式之狀態(tài)模式
JavaScript 核心(晉級(jí)高手必讀篇)
設(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
編寫(xiě)高質(zhì)量 JavaScript 代碼的基本要點(diǎn)
求值策略
閉包(Closures)
對(duì)象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計(jì)模式之代理模式
變量對(duì)象(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ì)模式之原型模式
根本沒(méi)有“JSON 對(duì)象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實(shí)現(xiàn)
全面解析 Module 模式
對(duì)象創(chuàng)建模式(下篇)
設(shè)計(jì)模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開(kāi)閉原則 OCP
設(shè)計(jì)模式之橋接模式
設(shè)計(jì)模式之策略模式
設(shè)計(jì)模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計(jì)模式之工廠模式

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

我們首先來(lái)看一下對(duì)象[Object]的概念,這也是 ECMASript 中最基本的概念。

對(duì)象 Object

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

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

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

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

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

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

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

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

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

原型鏈(Prototype chain)

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

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

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

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

類似于類”A”,”B”,”C”,在ECMAScript中尼創(chuàng)建對(duì)象類”a”,”b”,”c”,相應(yīng)地, 對(duì)象“a” 擁有對(duì)象“b”和”c”的共同部分。同時(shí)對(duì)象“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)用繼承過(guò)來(lái)的方法
b.calculate(30); // 60
c.calculate(40); // 80

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

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

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

如果一個(gè)對(duì)象的 prototype 沒(méi)有顯示的聲明過(guò)或定義過(guò),那么__prototype__的默認(rèn)值就是 object.prototype,而 object.prototype 也會(huì)有一個(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. 原型鏈

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

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

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

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

// 構(gòu)造函數(shù)
function Foo(y) {
  // 構(gòu)造函數(shù)將會(huì)以特定模式創(chuàng)建對(duì)象:被創(chuàng)建的對(duì)象都會(huì)有"y"屬性
  this.y = y;
}
// "Foo.prototype"存放了新建對(duì)象的原型引用
// 所以我們可以將之用于定義繼承和共享屬性或方法
// 所以,和上例一樣,我們有了如下代碼:
// 繼承屬性"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"自動(dòng)創(chuàng)建了一個(gè)特殊的屬性"constructor"
  // 指向a的構(gòu)造函數(shù)本身
  // 實(shí)例"b"和"c"可以通過(guò)授權(quán)找到它并用以檢測(cè)自己的構(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ù)與對(duì)象之間的關(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è)問(wèn)題完整和詳細(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),專門(mén)講述了 ECMAScript 中的面向?qū)ο缶幊獭?/p>

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

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

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

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

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

function foo(bar) {}
// 調(diào)用相同的function,每次都會(huì)產(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)用下去。邏輯上來(lái)說(shuō),這種實(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 就會(huì)暫停它自身的執(zhí)行,然后將控制權(quán)交給這個(gè) callee. 于是這個(gè) callee 被放入堆棧,稱為進(jìn)行中的上下文[running/active execution context]。當(dāng)這個(gè) callee 的上下文結(jié)束之后,會(huì)把控制權(quán)再次交給它的 caller,然后 caller 會(huì)在剛才暫停的地方繼續(xù)執(zhí)行。在這個(gè) caller 結(jié)束之后,會(huì)繼續(xù)觸發(fā)其他的上下文。一個(gè) callee 可以用返回(return)或者拋出異常(exception)來(lái)結(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)一段程序開(kāi)始時(shí),會(huì)先進(jìn)入全局執(zhí)行上下文環(huán)境[global execution context], 這個(gè)也是堆棧中最底部的元素。此全局程序會(huì)開(kāi)始初始化,初始化生成必要的對(duì)象[objects]和函數(shù)[functions]. 在此全局上下文執(zhí)行的過(guò)程中,它可能會(huì)激活一些方法(當(dāng)然是已經(jīng)初始化過(guò)的),然后進(jìn)入他們的上下文環(huán)境,然后將新的元素壓入堆棧。在這些初始化都結(jié)束之后,這個(gè)系統(tǒng)會(huì)等待一些事件(例如用戶的鼠標(biāo)點(diǎn)擊等),會(huì)觸發(fā)一些方法,然后進(jìn)入一個(gè)新的上下文環(huán)境。

見(jià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)容請(qǐng)查閱本系列教程的第11章執(zhí)行上下文(Execution context)。

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

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

一個(gè)執(zhí)行的上下文可以抽象的理解為 object。每一個(gè)執(zhí)行的上下文都有一系列的屬性(我們稱為上下文狀態(tài)),他們用來(lá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è)所需要的屬性(變量對(duì)象(variable object)this 指針(this value),作用域鏈(scope chain) ),執(zhí)行上下文根據(jù)具體實(shí)現(xiàn)還可以具有任意額外屬性。接著,讓我們仔細(xì)來(lái)看看這三個(gè)屬性。

變量對(duì)象(Variable Object)

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

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

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

讓我們看看下面例子中的全局執(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沒(méi)有被定義

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

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

圖 7. 全局變量對(duì)象

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

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

活動(dòng)對(duì)象(activation object)

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

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

考慮下面的情況:

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

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

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

圖 8. 激活對(duì)象

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

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

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

作用域鏈(Scope Chains)

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

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

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

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

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

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

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

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

圖 9. 作用域鏈

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

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

我們?cè)倏聪旅孢@個(gè)例子:

Object.prototype.x = 10;
var w = 20;
var y = 30;
// 在SpiderMonkey全局對(duì)象里
// 例如,全局上下文的變量對(duì)象是從"Object.prototype"繼承到的
// 所以我們可以得到“沒(méi)有聲明的全局變量”
// 因?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"對(duì)象從作用域鏈刪除之后
  // x又可以從foo的上下文中得到了,注意這次值又回到了100哦
  // "w" 也是局部變量
  console.log(x, w); // 100, 40
  // 在瀏覽器里
  // 我們可以通過(guò)如下語(yǔ)句來(lái)得到全局的w值
  console.log(window.w); // 20
})();

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

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

圖 10. with 增大的作用域鏈

注意,不是所有的全局對(duì)象都是由 Object.prototype 繼承而來(lái)的。上述圖示的情況可以在 SpiderMonkey 中測(cè)試。

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

閉包(Closures)

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

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

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

Scope chain = Activation object + [[Scope]]
作用域鏈 = 活動(dòng)對(duì)象 + [[Scope]]

請(qǐng)注意,最主要的事情是——函數(shù)在被創(chuàng)建時(shí)保存外部作用域,是因?yàn)檫@個(gè) 被保存的作用域鏈(saved scope chain) 將會(huì)在未來(lái)的函數(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]]中搜尋到的。理論上來(lái)說(shuō),也會(huì)有動(dòng)態(tài)作用域[dynamic scope], 也就是上述的 x 被解釋為 20,而不是 10. 但是 EMCAScript 不使用動(dòng)態(tài)作用域。

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

請(qǐng)看下面的例子:

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

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

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

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

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

上述代碼可以用這張圖來(lái)表示:

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

圖 11. 共享的[[Scope]]

在某個(gè)循環(huán)中創(chuàng)建多個(gè)函數(shù)時(shí),上圖會(huì)引發(fā)一個(gè)困惑。如果在創(chuàng)建的函數(shù)中使用循環(huán)變量(如”k”),那么所有的函數(shù)都使用同樣的循環(huán)變量,導(dǎo)致一些程序員經(jīng)常會(huì)得不到預(yù)期值?,F(xiàn)在清楚為什么會(huì)產(chǎn)生如此問(wè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

有一些用以解決這類問(wèn)題的技術(shù)。其中一種技巧是在作用域鏈中提供一個(gè)額外的對(duì)象,比如增加一個(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é)將會(huì)討論一個(gè)執(zhí)行上下文的最后一個(gè)屬性——this 指針的概念。

This 指針

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

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

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

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

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

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

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

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

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

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

總結(jié)(Conclusion)

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

同步與推薦

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