原文: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
的原型的引用器净。
這些原型需要什么?讓我們考慮下原型鏈的概念來(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的繼承體系痴施。
?
注意: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)系:
?
圖片再次展示了每個(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)境:
?
當(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)境有以下堆棧變化。
這就是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):
除了這三個(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)將有如下屬性:
再看一次旧烧,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ì)象:
并且函數(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]]
屬性中):
在代碼執(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è)原型鏈):
注意咒钟,并非所有實(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)于funargs
和function 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
);
這段代碼可以用以下插圖表示:
正是這個(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ò)let
或const
關(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