2019-08-15 JavaScript 核心

原文:JavaScript. The Core
作者:Dmitry Soshnikov
第二版:JavaScript. The Core: 2nd Edition

目錄

  • 1.對(duì)象

  • 2.原型鏈

  • 3.構(gòu)造器

  • 4.運(yùn)行棧

  • 5.運(yùn)行環(huán)境

  • 6.變量

  • 7.激活

  • 8.作用域

  • 9.閉包

  • 10.this

  • 11.結(jié)論

本文是ECMA-262-3規(guī)范系列的概述和摘要。每個(gè)章節(jié)都包含對(duì)應(yīng)匹配章節(jié)的引用,以便您可以閱讀以獲取更深入的理解丑念。

面向讀者:有經(jīng)驗(yàn)的開(kāi)發(fā)者驹饺,專(zhuān)家浓利。

我們從一個(gè)對(duì)象的概念觸發(fā)鲸拥,這是ECMAScript的基礎(chǔ)帆锋。

對(duì)象

ECMAScript是一門(mén)高度抽象的珍逸、面向?qū)ο蟮恼Z(yǔ)言逐虚,它處理對(duì)象。還有原始值谆膳,但是在需要的情況下也會(huì)轉(zhuǎn)換成對(duì)象叭爱。

對(duì)象是一個(gè)屬性的集合并具有單個(gè)原型對(duì)象,原型對(duì)象可能是另一個(gè)對(duì)象或者null值漱病。

我們來(lái)看一個(gè)對(duì)象的簡(jiǎn)單例子买雾,一個(gè)對(duì)象的原型被對(duì)象上的內(nèi)部屬性[[Prototype]]引用。然而缨称,在圖中的們將使用__<internal-property>__下劃線表示法而不是雙括號(hào)凝果,特別是對(duì)原型對(duì)象:__proto__

有如下代碼:

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

我們有一個(gè)擁有兩個(gè)顯式自有屬性和一個(gè)隱式__proto__屬性的對(duì)象睦尽,它是foo的原型的引用器净。

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

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

原型鏈

原型對(duì)象也只是簡(jiǎn)單的對(duì)象山害,可能有自己的原型。如果一個(gè)原型在它的prototype上有一個(gè)非空的引用沿量,亦或者有多個(gè)浪慌,這就稱(chēng)為原型鏈。

一條原型鏈?zhǔn)怯邢迋€(gè)對(duì)象的鏈接關(guān)系朴则,原型鏈常被用于實(shí)現(xiàn)繼承和屬性共享权纤。

考慮一下這種情況,當(dāng)我們有兩個(gè)對(duì)象乌妒,他們僅僅在小部分上有不同的地方汹想,其他全部都是相同的。很明顯撤蚊,對(duì)于一個(gè)設(shè)計(jì)良好的系統(tǒng)古掏,我們樂(lè)意去復(fù)用那些相似的功能或者代碼而不是在每個(gè)對(duì)象中重復(fù)它們。在基于類(lèi)的系統(tǒng)中侦啸,這種代碼復(fù)用學(xué)說(shuō)被稱(chēng)為基于類(lèi)的繼承—你將相似的功能放到類(lèi)A里槽唾,提供繼承自類(lèi)A的類(lèi)B和類(lèi)C丧枪,類(lèi)B和類(lèi)C有自己額外的小改動(dòng)。

ECMAScript沒(méi)有類(lèi)的概念庞萍。然而拧烦,代碼復(fù)用學(xué)說(shuō)沒(méi)有太多不同(盡管在某些種程度上比類(lèi)更加靈活)并通過(guò)原型鏈實(shí)現(xiàn)。這種繼承稱(chēng)為基于委托的繼承(或者和ECMAScript相近:基于原型的繼承)挂绰。

相似性如例子中的類(lèi)A屎篱、B和C,用ECMAScript創(chuàng)建對(duì)象a葵蒂、b和c交播。這樣,對(duì)象a存儲(chǔ)了對(duì)象b和c的公共部分践付。b和c僅存儲(chǔ)它們額外的屬性或方法秦士。

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

夠簡(jiǎn)單吧?我們可以看到b和c訪問(wèn)了定義在對(duì)象a上的calculate方法永高。這是通過(guò)原型鏈實(shí)現(xiàn)的隧土。

這個(gè)規(guī)則很簡(jiǎn)單:如果屬性a或方法a在對(duì)象內(nèi)找不到(即這個(gè)對(duì)象沒(méi)有這個(gè)自有屬性),然后就會(huì)嘗試在原型鏈上找這個(gè)屬性或方法命爬。如果屬性在原型上找不到曹傀,就會(huì)考慮在原型的原型上去找,即整個(gè)原型鏈去搜尋(和基于類(lèi)繼承完全相同饲宛,解析繼承的時(shí)候會(huì)遍歷類(lèi)鏈)皆愉。首次被找到的同名屬性或方法會(huì)被拿來(lái)用。這樣艇抠,找到的屬性稱(chēng)為繼承屬性幕庐。如果整個(gè)原型鏈都找不到這個(gè)屬性,就會(huì)返回undefined家淤。

注意异剥,this值在使用繼承方法時(shí)會(huì)被設(shè)置成原始對(duì)象,而不是找到該方法的原型對(duì)象絮重。即如上面的例子中this.y是取自b和c而不是a冤寿。然而this.x是兩次通過(guò)原型鏈機(jī)制取自a。

如果一個(gè)對(duì)象的原型沒(méi)有被顯示指定青伤,__proto__的默認(rèn)值就會(huì)設(shè)置成Object.prototype督怜。對(duì)象Object.prototype本身也有一個(gè)__proto__屬性,在原型鏈末端是null值潮模。

下圖展示了對(duì)象a亮蛔、b和c的繼承體系痴施。

圖 2.原型鏈

?
注意:ES5提供了一個(gè)標(biāo)準(zhǔn)化的可替代原型繼承的方式:使用Object.create方法

var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

你可以在對(duì)應(yīng)章節(jié)中查看更多ES5的新API擎厢。

盡管ES6標(biāo)準(zhǔn)化了__proto__究流,它仍然可以用來(lái)初始化對(duì)象。

這在讓對(duì)象擁有相同或相似的狀態(tài)結(jié)構(gòu)(即相同的屬性集)和不同的狀態(tài)值的情況下通常是需要的动遭。這種情況我們可以用指定模式的構(gòu)造函數(shù)去生產(chǎn)對(duì)象芬探。

構(gòu)造器

除了通過(guò)指定模式創(chuàng)建對(duì)象,構(gòu)造函數(shù)還做了其他一件有用的事 —— 自動(dòng)給新創(chuàng)建的對(duì)象設(shè)置原型對(duì)象厘惦。這個(gè)原型對(duì)象存在存儲(chǔ)在構(gòu)造函數(shù)的prototype屬性上偷仿。

例如,我們可以用構(gòu)造函數(shù)重寫(xiě)前一個(gè)例子的b和c宵蕉。這樣對(duì)象a的扮演了Foo的prototype角色:

// 構(gòu)造函數(shù)
function Foo(y) {
  // 可以通過(guò)具體模式創(chuàng)建對(duì)象:創(chuàng)建后有自由屬性"y"
  this.y = y;
}

// Foo.prototype儲(chǔ)存了新建對(duì)象的原型
// 如此我們可以用它定義共享/繼承屬性或方法

// 和前一個(gè)例子一樣有一個(gè)繼承屬性"x"
Foo.prototype.x = 10;
 
// 以及繼承方法"calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
 
// 再用Foo模式創(chuàng)建b和c
var b = new Foo(20);
var c = new Foo(30);
 
// 調(diào)用繼承方法
b.calculate(30); // 60
c.calculate(40); // 80
 
// 看下如我們所期待的引用屬性
 
console.log(
 
  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true
 
  // Foo.prototype自動(dòng)創(chuàng)建了"constructor"屬性酝静,
  // 該屬性是構(gòu)造函數(shù)自身的引用
  // 實(shí)例b和c可以通過(guò)委托找到構(gòu)造器并檢查它
 
  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ù)和對(duì)象關(guān)系

?

圖片再次展示了每個(gè)對(duì)象都有一個(gè)原型。構(gòu)造方法Foo有它自己的__proto__Function.prototype羡玛,其又通過(guò)__proto__屬性再次引用到Object.prototype别智,如此反復(fù),Foo.prototype只是Foo的一個(gè)顯式屬性稼稿,是對(duì)象b和c原型的引用薄榛。

形式上,如果要考慮分類(lèi)的概念(剛才我們已經(jīng)分類(lèi)的那個(gè)新分離的東西-Foo)让歼,構(gòu)造函數(shù)和原型對(duì)象的組合可以被稱(chēng)為“類(lèi)”敞恋。事實(shí)上,例如Python的第一類(lèi)動(dòng)態(tài)類(lèi)具有完全相同的屬性/方法解析實(shí)現(xiàn)谋右。從這個(gè)角度看硬猫,Python類(lèi)知識(shí)ECMAScript中使用的基于委托的繼承的語(yǔ)法糖。

注意:ES6中類(lèi)的概念已經(jīng)被標(biāo)準(zhǔn)化倚评,并且如上述在構(gòu)造函數(shù)之上完全實(shí)現(xiàn)為語(yǔ)法糖浦徊。從這個(gè)角度看,原型鏈?zhǔn)穷?lèi)繼承的實(shí)現(xiàn)細(xì)節(jié):

// ES6
class Foo {
  constructor(name) {
    this._name = name;
  }
 
  getName() {
    return this._name;
  }
}
 
class Bar extends Foo {
  getName() {
    return super.getName() + ' Doe';
  }
}
 
var bar = new Bar('John');
console.log(bar.getName()); // John Doe

有關(guān)該主題的完整詳細(xì)說(shuō)明天梧,請(qǐng)參閱ES3系列的第7章曹鸠。有兩部分:

第7.1章 OOP封救。常規(guī)理論上,您將在其中找到各種OOP范例和文學(xué)描述,以及它們和ECMAScript和第7.2章的比較送粱。

OOP.ECMAScript實(shí)現(xiàn)是專(zhuān)門(mén)用于ECMAScript中的OOP

OOP: Object Oriented Programming,面向?qū)ο缶幊?/p>

現(xiàn)在胁勺,當(dāng)我們了解基本對(duì)象方面時(shí)怀愧,讓我們看看ECMAScript中運(yùn)行時(shí)程序是如何實(shí)現(xiàn)的。也就是所謂的執(zhí)行上下文堆棧挫酿,每個(gè)元素都可以抽象地表示為對(duì)象构眯。確實(shí),ECMAScript幾乎在任何地方都以對(duì)象的概念運(yùn)作早龟。

執(zhí)行環(huán)境堆棧

ECMAScript中有代碼有三種類(lèi)型:全局代碼惫霸、函數(shù)代碼和eval代碼猫缭。每種代碼都在其執(zhí)行環(huán)境中進(jìn)行評(píng)估。只有一個(gè)全局環(huán)境壹店,但可能有很多函數(shù)或eval執(zhí)行環(huán)境猜丹。每次調(diào)用函數(shù),進(jìn)入函數(shù)執(zhí)行環(huán)境并評(píng)估執(zhí)行函數(shù)代碼類(lèi)型硅卢。每次調(diào)用eval函數(shù),都會(huì)進(jìn)入eval執(zhí)行環(huán)境評(píng)估執(zhí)行其代碼将塑。

注意点寥,一個(gè)函數(shù)可能會(huì)生成無(wú)限的環(huán)境集开财,因?yàn)槊看握{(diào)用函數(shù)(即使是遞歸調(diào)用自身)都會(huì)產(chǎn)生一個(gè)帶有新上下文狀態(tài)的環(huán)境:

function foo(bar) {}

// 調(diào)用相同函數(shù)责鳍,每次調(diào)用以不同的上下文狀態(tài)(如argument中的"bar"的值)生成三個(gè)不同的上下文
foo(10);
foo(20);
foo(30);

一個(gè)執(zhí)行環(huán)境可以激活另一個(gè)環(huán)境,例如历葛,函數(shù)調(diào)用另一個(gè)函數(shù)(或者全局函環(huán)境下調(diào)用全局函數(shù))正塌,等等。邏輯上恤溶,這是作為堆棧實(shí)現(xiàn)的乓诽,稱(chēng)為執(zhí)行環(huán)境堆棧。

激活另一個(gè)環(huán)境的環(huán)境稱(chēng)為調(diào)用者咒程。正在激活的環(huán)境稱(chēng)為被調(diào)用者鸠天。被調(diào)用者此時(shí)也可能是一個(gè)調(diào)用者(例如,從全局環(huán)境調(diào)用的函數(shù)帐姻,該函數(shù)調(diào)用一些內(nèi)部函數(shù))稠集。

當(dāng)調(diào)用者激活(調(diào)用)一個(gè)被調(diào)用者時(shí),調(diào)用者暫停其執(zhí)行并將控制流傳遞鬼被調(diào)用者饥瓷。被調(diào)用者被推入堆棧并成為正在運(yùn)行(激活的)執(zhí)行環(huán)境剥纷。在被調(diào)用者的環(huán)境結(jié)束后,控制權(quán)回交給調(diào)用者呢铆,調(diào)用者的環(huán)境代碼繼續(xù)評(píng)估執(zhí)行(也可以激活其他環(huán)境)直到結(jié)束晦鞋,以此類(lèi)推悠垛。被調(diào)用者可能只是簡(jiǎn)單返回或異常退出因俐。拋出一個(gè)未捕獲的異常可能退出(從堆棧中彈出)一個(gè)或多個(gè)環(huán)境胡嘿。

即所有ECMAScript程序運(yùn)行時(shí)都表示為執(zhí)行環(huán)境(EC)堆棧拓瞪,其中棧頂是激活的環(huán)境:

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

?

當(dāng)程序開(kāi)始時(shí),它進(jìn)入全局環(huán)境,是棧的底部即第一個(gè)元素呻拌。然后全局代碼提供一些初始化操作复亏,創(chuàng)建需要的對(duì)象和函數(shù)。在執(zhí)行全局環(huán)境期間评架,代碼可以激活一些其他(已創(chuàng)建的)函數(shù),該函數(shù)進(jìn)入其執(zhí)行環(huán)境,將新元素推到堆棧,以此類(lèi)推南窗。初始化完成后呜袁,運(yùn)行時(shí)系統(tǒng)正在等待一些事件(如用戶(hù)的鼠標(biāo)點(diǎn)擊),這將激活某些功能并進(jìn)入新的執(zhí)行環(huán)境芜抒。

在下圖中屯耸,將一些函數(shù)環(huán)境作為EC1,全局環(huán)境作為Global EC缓淹,我們?cè)谶M(jìn)入和退出EC1時(shí),全局環(huán)境有以下堆棧變化。

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

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

有關(guān)ECMAScript中執(zhí)行環(huán)境的更多信息吓肋,請(qǐng)參閱相應(yīng)的第一章 執(zhí)行環(huán)境

正如我們所說(shuō)李剖,堆棧中的每個(gè)執(zhí)行環(huán)境都可以表示為一個(gè)對(duì)象充择。讓我們看看一個(gè)環(huán)境執(zhí)行它的代碼需要什么樣的結(jié)構(gòu)和狀態(tài)(其屬性)類(lèi)型。

執(zhí)行環(huán)境

一個(gè)執(zhí)行環(huán)境可以抽象地表示為一個(gè)簡(jiǎn)單對(duì)象。每個(gè)執(zhí)行環(huán)境都有一組屬性(我們稱(chēng)環(huán)境狀態(tài))以跟蹤其關(guān)聯(lián)代碼的執(zhí)行進(jìn)度造成。在下圖中,展示了環(huán)境的結(jié)構(gòu):

圖 6.執(zhí)行環(huán)境結(jié)構(gòu)

除了這三個(gè)需要的屬性(變量對(duì)象坐桩,作用域鏈和this值)之外,執(zhí)行環(huán)境可以具有任何其他狀態(tài)荆残,具體取決于實(shí)現(xiàn)像啼。

讓我們?cè)敿?xì)考慮一下環(huán)境的這些重要屬性真朗。

變量對(duì)象

一個(gè)變量對(duì)象是關(guān)聯(lián)執(zhí)行環(huán)境數(shù)據(jù)的容器。它是一個(gè)特殊的對(duì)象,存儲(chǔ)在環(huán)境中定義的變量和函數(shù)聲明。

注意清钥,函數(shù)表達(dá)式(與函數(shù)聲明不同)不包含在變量對(duì)象中怖侦。

變量對(duì)象是一個(gè)抽象的概念搬葬。在不同的環(huán)境類(lèi)型中,物理上疾忍,它使用不同的對(duì)象呈現(xiàn)聂渊。例如歹撒,在全局環(huán)境中,變量對(duì)象是全局對(duì)象自身(這就是為何我們能通過(guò)全局對(duì)象的屬性名去引用全局變量)竭望。

讓我們?cè)谌謭?zhí)行環(huán)境中考慮一下例子:

var foo = 10;
 
function bar() {} // function declaration, FD (函數(shù)聲明)
(function baz() {}); // function expression, FE (函數(shù)表達(dá)式)
 
console.log(
  this.foo == foo, // true
  window.bar == bar // true
);
 
console.log(baz); // ReferenceError, "baz" is not defined

然后全局環(huán)境變量(VO)將有如下屬性:

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

再看一次旧烧,baz作為函數(shù)表達(dá)式的函數(shù)不包含在變量對(duì)象中。這就是我們嘗試在函數(shù)本身之外訪問(wèn)它卻得到ReferenceError的原因廉赔。

注意。這與其他語(yǔ)言(如C/C++)不同,ECMAScript中攒至,只有函數(shù)創(chuàng)建一個(gè)新的作用域志膀。定義在一個(gè)函數(shù)作用域內(nèi)的變量和內(nèi)部函數(shù)在外部不是直接可見(jiàn)的蒋荚,也不會(huì)污染全局變量對(duì)象。

使用eval我們也會(huì)進(jìn)入一個(gè)新的執(zhí)行環(huán)境(eval的)。但是eval也使用全局變量對(duì)象或者調(diào)用者的變量對(duì)象(如從函數(shù)中調(diào)用eval)。

那么函數(shù)及其變量對(duì)象呢?抬纸?在函數(shù)環(huán)境中,變量對(duì)象被表示為一個(gè)激活對(duì)象坛猪。

激活對(duì)象

當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)脖阵,會(huì)創(chuàng)建一個(gè)稱(chēng)為激活對(duì)象的特殊對(duì)象。它被形參和特殊的arguments對(duì)象(形參的映射墅茉,具有索引屬性)填充命黔。然后激活對(duì)象被用作函數(shù)環(huán)境的變量對(duì)象。

即函數(shù)的變量對(duì)象同樣是簡(jiǎn)單變量對(duì)象坠宴,但變量和函數(shù)聲明之外隅忿,還存儲(chǔ)了形參和arguments對(duì)象纷责,稱(chēng)激活對(duì)象熙参。

考慮如下例子:

function foo(x, y) {
  var z = 30;
  function bar() {} // FD
  (function baz() {}); // FE
}
 
foo(10, 20);

我們有foo函數(shù)環(huán)境的下一個(gè)激活對(duì)象:

圖 8.激活對(duì)象

并且函數(shù)表達(dá)式baz也不包含在變量(激活)對(duì)象中全庸。

有關(guān)此主題的所有細(xì)微情況(如變量和函數(shù)的聲明提升)的完整描述可以再同一名稱(chēng)中找到。第2章 變量對(duì)象

注意,在ES5中谓媒,變量對(duì)象和激活對(duì)象的概念被合并到詞法環(huán)境模型中,詳細(xì)描述可以在對(duì)應(yīng)的章節(jié)中找到。

我們正在進(jìn)入下一部分赫冬。眾所周知寇漫,在ECMAScript中我們可以使用內(nèi)部函數(shù),在這些內(nèi)部函數(shù)中,我們可以引用父函數(shù)的變量或全局環(huán)境的變量划提。當(dāng)我們將變量對(duì)象命名為環(huán)境中的作用域?qū)ο笃妓洌蜕厦嬗懻摰脑玩滎?lèi)似,這是所謂的作用域鏈。

作用域鏈

作用域鏈?zhǔn)且粋€(gè)對(duì)象列表佩微,在環(huán)境的代碼中搜索出現(xiàn)的標(biāo)識(shí)符。

該規(guī)則也和原型鏈一樣簡(jiǎn)單:如果在自己的范圍(自己的變量/激活對(duì)象)中找不到變量,則在父變量對(duì)象上查找园细,以此類(lèi)推。

對(duì)于環(huán)境尖奔,標(biāo)識(shí)符是:變量名搭儒、函數(shù)聲明、形參等越锈。當(dāng)函數(shù)在內(nèi)部代碼中引用的不是局部變量(或內(nèi)部函數(shù)或形參)的標(biāo)識(shí)符時(shí)仗嗦,這種變量被稱(chēng)為自由變量,為了準(zhǔn)確查找這些自由變量甘凭,作用域鏈就派上用場(chǎng)了稀拐。

一般情況下,作用域鏈?zhǔn)撬羞@些父變量對(duì)象的列表丹弱,加上(在作用域前的)函數(shù)自己的變量/激活對(duì)象德撬。但是,作用域鏈還可以包含任何其他對(duì)象躲胳,例如環(huán)境執(zhí)行期間動(dòng)態(tài)添加到作用域鏈的對(duì)象蜓洪,例如with對(duì)象catch分句的特殊對(duì)象。

在解析(查找)標(biāo)識(shí)符時(shí)坯苹,從激活對(duì)象開(kāi)始搜索作用域鏈隆檀,然后(如果在自己的激活對(duì)象中找不到標(biāo)識(shí)符)直到作用域鏈的頂端 - 重復(fù)如此,也僅是和原型鏈相似罷了粹湃。

var x = 10;
 
(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" 和 "y" 是自由變量
    // 在bar的作用域(bar的激活對(duì)象)之后被找到
    // 作用域鏈?zhǔn)牵?bar -> foo -> global
    console.log(x + y + z);
  })();
})();

我們可以假定作用域鏈對(duì)象的聯(lián)系通過(guò)隱藏屬性__parent__恐仑,引用到作用域鏈的下一個(gè)對(duì)象。這種方法可以在真正的Rhino代碼中進(jìn)行測(cè)試为鳄,并且恰好這種技術(shù)被用于ES5詞法環(huán)境(稱(chēng)為外部鏈接)裳仆。作用域鏈的另一種表示可以是簡(jiǎn)單的數(shù)組。使用一個(gè)__parent__概念孤钦,我們可以用下圖表示上面的例子(因此父變量被保存在函數(shù)的[[Scope]]屬性中):

圖 9.作用域鏈

在代碼執(zhí)行時(shí)歧斟,可以使用with語(yǔ)句和catch從句對(duì)象來(lái)擴(kuò)充作用域鏈纯丸。由于這些對(duì)象是簡(jiǎn)單的對(duì)象,它們可能有原型(和原型鏈)静袖。這一事實(shí)導(dǎo)致作用域鏈查找的二維的:(1)首先考慮作用域鏈觉鼻,(2)然后在每個(gè)作用域鏈的鏈接上,深入原型鏈(如果鏈接有原型的情況)勾徽。

對(duì)于這個(gè)例子:

Object.prototype.x = 10;
 
var w = 20;
var y = 30;
 
// 在 SpiderMonkey 全局對(duì)象中
// 如全局環(huán)境的變量對(duì)象繼承自"Object.prototype"
// 則我們可以引用到一個(gè)未定義的變量x滑凉,它會(huì)在原來(lái)鏈上被查找到
console.log(x); // 10
 
(function foo() {
  // foo的本地變量
  var w = 40;
  var x = 100;
  
  // x在Object.prototype中被查找到
  // 因?yàn)閧z: 50} 繼承了它
  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }
 
  // 在with對(duì)象在原型鏈上移除后统扳,
  // x再次在foo環(huán)境的激活對(duì)象被查找到
  // w也是局部的
  console.log(x, w); // 100, 40
 
  // 這是我們?cè)跒g覽器宿主環(huán)境中顯式引用全局w變量的方法
  console.log(window.w); // 20
})();

我們有以下結(jié)構(gòu)(也就是說(shuō)喘帚,我們轉(zhuǎn)到__parent__之前,優(yōu)先考慮__proto__這個(gè)原型鏈):

圖 10.擴(kuò)展作用域鏈

注意咒钟,并非所有實(shí)現(xiàn)的全局對(duì)象都繼承自Object.prototype吹由。圖中描述的行為(引用來(lái)自全局環(huán)境的未定義的x變量)是可測(cè)試的,如在SpiderMonkey中朱嘴。

在所有父變量對(duì)象存在之前倾鲫,在函數(shù)內(nèi)部獲取父數(shù)據(jù)沒(méi)啥特別的 — 我們展示遍歷作用域鏈去解析(查找)需要的變量。但是萍嬉,和上面我們提到的乌昔,在一個(gè)環(huán)境結(jié)束后,它全部的狀態(tài)和自身都已被摧毀了壤追,同時(shí)內(nèi)部函數(shù)從父函數(shù)中返回磕道。此外,這個(gè)已返回的函數(shù)之后可能會(huì)被另一個(gè)環(huán)境激活行冰,如果一些自由變量的環(huán)境已經(jīng)消失溺蕉,這種激活會(huì)是什么?在一般理論中悼做,有助于解決這個(gè)問(wèn)題的是(詞法上)閉包的概念疯特,在ECMAScript中這是和作用域鏈直接關(guān)聯(lián)的。

閉包

在ECMAScript中肛走,函數(shù)是第一類(lèi)對(duì)象漓雅。這個(gè)術(shù)語(yǔ)意味著函數(shù)可以作為參數(shù)傳遞給其他函數(shù)(這種情況下,它們被稱(chēng)為funargs朽色,是function arguments的簡(jiǎn)稱(chēng))邻吞。接收funargs的函數(shù)稱(chēng)為高階函數(shù),或者更貼近數(shù)學(xué)地說(shuō)叫運(yùn)算符纵搁。從其他函數(shù)返回的函數(shù)稱(chēng)為函數(shù)值函數(shù)(或有函數(shù)值的函數(shù))吃衅。

有兩個(gè)關(guān)于funargsfunction values的概念性問(wèn)題。并且這兩個(gè)子問(wèn)題被概括稱(chēng)為Funarg問(wèn)題(或函數(shù)參數(shù)問(wèn)題)的問(wèn)題腾誉。為了精確地解決整個(gè)funarg問(wèn)題徘层,發(fā)明了閉包的概念峻呕。讓我們更加詳細(xì)地描述這兩個(gè)子問(wèn)題(我們將看到它們都是ECMAScript中在一個(gè)函數(shù)的圖形屬性[[Scope]]提到的)。

funcarg問(wèn)題的第一個(gè)子類(lèi)型是向上的funarg問(wèn)題趣效。當(dāng)一個(gè)函數(shù)從另一個(gè)函數(shù)向上返回(回到外部)并且使用之前已經(jīng)提到的自由變量瘦癌。為了能夠訪問(wèn)父環(huán)境甚至甚至父環(huán)境接收后的變量,內(nèi)部函數(shù)在創(chuàng)建時(shí)在[[Scope]]屬性中保存了父環(huán)境的作用域鏈跷敬。然后該內(nèi)部函數(shù)激活時(shí)讯私,它的環(huán)境的作用域鏈形成激活對(duì)象和此[[Scope]]屬性的組合(實(shí)際上是我們剛才在圖中看見(jiàn)的內(nèi)容):

Scope chain = Activation object + [[Scope]] // 作用域鏈 = 激活對(duì)象 + [[Scope]]

再注意一下主要的事情 — 在創(chuàng)建時(shí) — 函數(shù)保存父作用域,因?yàn)檫@個(gè)保存的作用域鏈將會(huì)用于在函數(shù)進(jìn)一步調(diào)用中的變量查找西傀。

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

// foo 返回一個(gè)函數(shù)斤寇,返回的函數(shù)使用了變量 x
var returnedFunction = foo();
 
// 全局變量 x
var x = 20;
 
// 返回函數(shù)的執(zhí)行
returnedFunction(); // 10,而不是 20

這種作用域風(fēng)格稱(chēng)為靜態(tài)(詞法)作用域拥褂。我們看到變量x在返回函數(shù)保存的[[Socpe]]中找到娘锁。一般理論中,當(dāng)例子中的變量x被解析(查找)為20時(shí)饺鹃,這也是動(dòng)態(tài)作用域莫秆。只是,ECMAScript中并未使用動(dòng)態(tài)作用域悔详。

funarg問(wèn)題的第二部分是向下的funarg問(wèn)題镊屎,這種情況下父環(huán)境可以存在,但可能是解析標(biāo)識(shí)符的歧義茄螃。問(wèn)題是:從哪個(gè)作用域使用標(biāo)識(shí)符的值 — 在函數(shù)創(chuàng)建時(shí)靜態(tài)保存還是執(zhí)行的時(shí)候動(dòng)態(tài)形成(即調(diào)用者的作用域)缝驳?為了避免這種歧義,形成閉包责蝠,就決定使用靜態(tài)作用域:

// 全局 x
var x = 10;
// 全局函數(shù)
function foo() {
  console.log(x);
}
 
(function (funarg) {
  // 局部 x
  var x = 20;
  // 這是沒(méi)有歧義的党巾,因?yàn)槲覀冇昧嗽趂oo中靜態(tài)保存到[[Scope]]的全局x
  // 而不是激活了函數(shù)參數(shù)的調(diào)用者(這個(gè)匿名立即執(zhí)行函數(shù))作用域中的 x
  funarg(); // 10, 而不是 20
})(foo); // foo 通過(guò)`向下`作為函數(shù)參數(shù)

我們可以得出結(jié)論,在語(yǔ)言中使用閉包霜医,靜態(tài)作用域是強(qiáng)制性要求齿拂。但是某些語(yǔ)言可能會(huì)提供動(dòng)態(tài)和靜態(tài)作用域的組合,允許程序員選擇關(guān)閉什么打開(kāi)什么肴敛。因?yàn)樵贓CMAScript中只是用靜態(tài)作用域(即我們對(duì)funarg問(wèn)題的兩個(gè)子類(lèi)型都有解決方案)署海,結(jié)論是:ECMAScript完全支持閉包,技術(shù)上是使用函數(shù)[[Scope]]屬性去實(shí)現(xiàn)的∫侥校現(xiàn)在我們可以給出一個(gè)對(duì)閉包的正確定義:

閉包是代碼塊(在ECMAScript中是一個(gè)函數(shù))和靜態(tài)/詞法保存父作用域的組合砸狞。因此,通過(guò)這些保存的作用域镀梭,函數(shù)可以輕松引用到自由變量刀森。

注意。每個(gè)(普通)函數(shù)創(chuàng)建時(shí)[[Scope]]都會(huì)保存报账,理論上研底,在ECMAScript中全部函數(shù)都是閉包埠偿。

另一個(gè)需要注意的重要事項(xiàng),一個(gè)函數(shù)可能擁有相同的父作用域(例如榜晦,我們有兩個(gè)內(nèi)部/全局函數(shù)冠蒋,這是非常正常的情況)。這種情況下乾胶,保存在[[Scope]]屬性中的變量是被擁有相同父作用域鏈的全部函數(shù)共享的抖剿。一個(gè)閉包對(duì)變量所做的改變反應(yīng)在另一個(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
);

這段代碼可以用以下插圖表示:

圖 11.共享的[[Scope]]

正是這個(gè)特性與在循環(huán)中創(chuàng)建多個(gè)函數(shù)的混淆是相關(guān)的识窿。在創(chuàng)建的函數(shù)內(nèi)部使用循環(huán)計(jì)數(shù)器斩郎,當(dāng)所有函數(shù)內(nèi)擁有相同的計(jì)數(shù)器值,一些程序員經(jīng)常會(huì)得到以外的結(jié)果⊥蠓觯現(xiàn)在應(yīng)該清楚為什么會(huì)這樣 — 因?yàn)檫@些函數(shù)擁有相同的[[Scope]]孽拷,循環(huán)計(jì)數(shù)器擁有最后賦值的值吨掌。

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    console.log(k);
  };
}
 
data[0](); // 3, 而不是 0
data[1](); // 3, 而不是 1
data[2](); // 3, 而不是 2

有幾種技術(shù)可以解決這個(gè)問(wèn)題半抱。其中一種是在作用域鏈中提供一個(gè)額外的對(duì)象 — 例如使用額外方法:

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      console.log(x);
    };
  })(k); // 傳遞 k 值
}
 
// 現(xiàn)在是正確的了
data[0](); // 0
data[1](); // 1
data[2](); // 2

注意:ES6引入了塊級(jí)作用域綁定。通過(guò)letconst關(guān)鍵字來(lái)完成膜宋。如以上例子可以簡(jiǎn)單便捷地重寫(xiě):

let data = [];
 
for (let k = 0; k < 3; k++) {
  data[k] = function () {
    console.log(k);
  };
}
 
data[0](); // 0
data[1](); // 1
data[2](); // 2

那些有興趣對(duì)閉包理論和實(shí)際應(yīng)用有更深入的人可以再第六章 閉包找到更多信息窿侈。想獲得關(guān)于作用域鏈的更多信息,請(qǐng)查看第四章 作用域鏈秋茫。

考慮到執(zhí)行環(huán)境的最后一個(gè)屬性史简,我們即將進(jìn)入下一部分,關(guān)于this值的概念肛著。

This 值

this值是一個(gè)關(guān)聯(lián)執(zhí)行環(huán)境的特殊對(duì)象圆兵,因此,它可以被命名為環(huán)境對(duì)象(即在所激活執(zhí)行環(huán)境的環(huán)境對(duì)象)枢贿。

任何對(duì)象都能被用作環(huán)境的this值殉农。一個(gè)重要的點(diǎn)是this值是執(zhí)行環(huán)境的屬性,而不是變量對(duì)象的屬性局荚。

這個(gè)特性非常重要超凳,因?yàn)楹妥兞肯啾龋?code>this值從不參與標(biāo)識(shí)符的解析過(guò)程。即在代碼中訪問(wèn)this是耀态,它的值直接來(lái)自執(zhí)行環(huán)境轮傍,不經(jīng)過(guò)作用域鏈查找。this值在進(jìn)入執(zhí)行環(huán)境時(shí)只確定一次首装。

注意:在ES6中this實(shí)際上稱(chēng)為詞法環(huán)境的一個(gè)屬性创夜,即ES3屬于中變量對(duì)象的屬性。這樣做是為了支持從父環(huán)境中繼承的仙逻、有詞法上的this的箭頭函數(shù)驰吓。

順便一提揍魂,和ECMAScript相反,例如Python有它的函數(shù)self參數(shù)作為簡(jiǎn)單變量解析棚瘟,可以在執(zhí)行期間改變成另外的值现斋。在ECMAScript中是不可能對(duì)this賦新的值的,重復(fù)一遍偎蘸,this不是變量庄蹋,也不存放在變量對(duì)象中。

在全局環(huán)境中迷雪,this值是全局對(duì)象自身(這意味著限书,this值和變量對(duì)象相等):

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

在函數(shù)環(huán)境的情況,this值在每個(gè)單獨(dú)調(diào)用的函數(shù)中可能不同章咧。this值由調(diào)用者通過(guò)調(diào)用表達(dá)式(例如倦西,函數(shù)激活的方式)的形式來(lái)提供。舉例子赁严,如下的foo是一個(gè)被調(diào)用者扰柠,從全局環(huán)境(它是一個(gè)調(diào)用者)中被調(diào)用窍仰∮募撸看下面例子,如何對(duì)相同代碼的函數(shù)谣辞,this值是如何被不同調(diào)用者在不同的調(diào)用(不同的函數(shù)激活方式)中提供的:

// 函數(shù)foo的代碼從未改變程剥,但 this 值在每次激活都不同
function foo() {
  alert(this);
}
 
// 調(diào)用者激活 foo(被調(diào)用者) 劝枣,為被調(diào)用者提供 this 值
foo(); // 全局對(duì)象
foo.prototype.constructor(); // foo的原型
 
var bar = {
  baz: foo
};
 
bar.baz(); // bar
(bar.baz)(); // 也是 bar
(bar.baz = bar.baz)(); // 但這是全局對(duì)象
(bar.baz, bar.baz)(); // 也是全局對(duì)象
(false || bar.baz)(); // 也是全局對(duì)象
 
var otherFoo = bar.baz;
otherFoo(); // 又是全局對(duì)象

為了深入考慮this在每次函數(shù)調(diào)用時(shí)可能改變(可能更重要),你可以閱讀第三章 This织鲸,這將詳細(xì)討論上述案例舔腾。

結(jié)論

到這一步,我們完成了這個(gè)簡(jiǎn)單的概述搂擦。雖然事實(shí)并非如此簡(jiǎn)短稳诚,對(duì)某些這題的正題解釋需要一本完整的書(shū)。我們沒(méi)有觸及的兩個(gè)主題:函數(shù)(以及函數(shù)類(lèi)型的差異盾饮,例如函數(shù)聲明和函數(shù)表達(dá)式)和ECMAScript中使用的評(píng)估策略采桃。這兩個(gè)主題可以在ES3系列的對(duì)應(yīng)章節(jié)中找到:第五章 函數(shù)第八章 評(píng)估策略

如果您有意見(jiàn)丘损、問(wèn)題或補(bǔ)充普办,我很樂(lè)意在評(píng)論中看到你們。

祝你在學(xué)習(xí)ECMAScript學(xué)習(xí)中好運(yùn)徘钥!

撰稿: Dmitry A. Soshnikov
發(fā)布于: 2010-09-02

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衔蹲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舆驶,老刑警劉巖橱健,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異沙廉,居然都是意外死亡拘荡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)撬陵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)珊皿,“玉大人,你說(shuō)我怎么就攤上這事巨税◇ǎ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵草添,是天一觀的道長(zhǎng)驶兜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)远寸,這世上最難降的妖魔是什么抄淑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮而晒,結(jié)果婚禮上蝇狼,老公的妹妹穿的比我還像新娘。我一直安慰自己倡怎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布贱枣。 她就那樣靜靜地躺著监署,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纽哥。 梳的紋絲不亂的頭發(fā)上钠乏,一...
    開(kāi)封第一講書(shū)人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音春塌,去河邊找鬼晓避。 笑死,一個(gè)胖子當(dāng)著我的面吹牛只壳,可吹牛的內(nèi)容都是我干的俏拱。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吼句,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼锅必!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起惕艳,我...
    開(kāi)封第一講書(shū)人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤搞隐,失蹤者是張志新(化名)和其女友劉穎驹愚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體劣纲,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逢捺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了癞季。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒸甜。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖余佛,靈堂內(nèi)的尸體忽然破棺而出柠新,到底是詐尸還是另有隱情,我是刑警寧澤辉巡,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布恨憎,位于F島的核電站,受9級(jí)特大地震影響郊楣,放射性物質(zhì)發(fā)生泄漏憔恳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一净蚤、第九天 我趴在偏房一處隱蔽的房頂上張望钥组。 院中可真熱鬧,春花似錦今瀑、人聲如沸程梦。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)屿附。三九已至,卻和暖如春哥童,著一層夾襖步出監(jiān)牢的瞬間挺份,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工贮懈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匀泊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓朵你,卻偏偏與公主長(zhǎng)得像各聘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子撬呢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容