原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
1.對(duì)象
2.原型鏈
8.作用域鏈
9.閉包
10.This
這篇文章是「深入ECMA-262-3」系列的一個(gè)概覽和摘要。每個(gè)部分都包含了對(duì)應(yīng)章節(jié)的鏈接,所以你可以閱讀它們以便對(duì)其有更深的理解。
面向讀者:經(jīng)驗(yàn)豐富的程序員,專家。
我們以思考對(duì)象的概念做為開始,這是ECMAScript的基礎(chǔ)。
對(duì)象
ECMAScript做為一個(gè)高度抽象的面向?qū)ο笳Z言,是通過對(duì)象來交互的。即使ECMAScript里邊也有基本類型,但是,當(dāng)需要的時(shí)候,它們也會(huì)被轉(zhuǎn)換成對(duì)象。
一個(gè)對(duì)象就是一個(gè)屬性集合,并擁有一個(gè)獨(dú)立的prototype(原型)對(duì)象。這個(gè)prototype可以是一個(gè)對(duì)象或者null。
讓我們看一個(gè)關(guān)于對(duì)象的基本例子。一個(gè)對(duì)象的prototype是以內(nèi)部的[[Prototype]]屬性來引用的。但是,在示意圖里邊我們將會(huì)使用____下劃線標(biāo)記來替代兩個(gè)括號(hào),對(duì)于prototype對(duì)象來說是:__proto__。
對(duì)于以下代碼:
var foo = {
x: 10,
y: 20
};
我們擁有一個(gè)這樣的結(jié)構(gòu),兩個(gè)明顯的自身屬性和一個(gè)隱含的__proto__屬性,這個(gè)屬性是對(duì)foo原型對(duì)象的引用:
這些prototype有什么用?讓我們以原型鏈(prototype chain)的概念來回答這個(gè)問題。
原型鏈
原型對(duì)象也是簡單的對(duì)象并且可以擁有它們自己的原型。如果一個(gè)原型對(duì)象的原型是一個(gè)非null的引用,那么以此類推,這就叫作原型鏈。
原型鏈?zhǔn)且粋€(gè)用來實(shí)現(xiàn)繼承和共享屬性的有限對(duì)象鏈。
考慮這么一個(gè)情況,我們擁有兩個(gè)對(duì)象,它們之間只有一小部分不同,其他部分都相同。顯然,對(duì)于一個(gè)設(shè)計(jì)良好的系統(tǒng),我們將會(huì)重用相似的功能/代碼,而不是在每個(gè)單獨(dú)的對(duì)象中重復(fù)它。在基于類的系統(tǒng)中,這個(gè)代碼重用風(fēng)格叫作類繼承-你把相似的功能放入類A中,然后類B和類C繼承類A,并且擁有它們自己的一些小的額外變動(dòng)。
ECMAScript中沒有類的概念。但是,代碼重用的風(fēng)格并沒有太多不同(盡管從某些方面來說比基于類(class-based)的方式要更加靈活)并且通過原型鏈來實(shí)現(xiàn)。這種繼承方式叫作委托繼承(delegation based inheritance)(或者,更貼近ECMAScript一些,叫作原型繼承(prototype based inheritance))。
跟例子中的類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
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
足夠簡單,是不是?我們看到b和c訪問到了在對(duì)象a中定義的calculate方法。這是通過原型鏈實(shí)現(xiàn)的。
規(guī)則很簡單:如果一個(gè)屬性或者一個(gè)方法在對(duì)象自身中無法找到(也就是對(duì)象自身沒有一個(gè)那樣的屬性),然后它會(huì)嘗試在原型鏈中尋找這個(gè)屬性/方法。如果這個(gè)屬性在原型中沒有查找到,那么將會(huì)查找這個(gè)原型的原型,以此類推,遍歷整個(gè)原型鏈(當(dāng)然這在類繼承中也是一樣的,當(dāng)解析一個(gè)繼承的方法的時(shí)候-我們遍歷class鏈( class chain))。第一個(gè)被查找到的同名屬性/方法會(huì)被使用。因此,一個(gè)被查找到的屬性叫作繼承屬性。如果在遍歷了整個(gè)原型鏈之后還是沒有查找到這個(gè)屬性的話,返回undefined值。
注意,繼承方法中所使用的this的值被設(shè)置為原始對(duì)象,而并不是在其中查找到這個(gè)方法的(原型)對(duì)象。也就是,在上面的例子中this.y取的是b和c中的值,而不是a中的值。但是,this.x是取的是a中的值,并且又一次通過原型鏈機(jī)制完成。
如果沒有明確為一個(gè)對(duì)象指定原型,那么它將會(huì)使用__proto__的默認(rèn)值-Object.prototype。Object.prototype對(duì)象自身也有一個(gè)__proto__屬性,這是原型鏈的終點(diǎn)并且值為null。
下一張圖展示了對(duì)象a,b,c之間的繼承層級(jí):
注意: ES5標(biāo)準(zhǔn)化了一個(gè)實(shí)現(xiàn)原型繼承的可選方法,即使用Object.create函數(shù):
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
你可以在對(duì)應(yīng)的章節(jié)獲取到更多關(guān)于ES5新API的信息。 ES6標(biāo)準(zhǔn)化了__proto__屬性,并且可以在對(duì)象初始化的時(shí)候使用它。
通常情況下需要對(duì)象擁有相同或者相似的狀態(tài)結(jié)構(gòu)(也就是相同的屬性集合),賦以不同的狀態(tài)值。在這個(gè)情況下我們可能需要使用構(gòu)造函數(shù)(constructor function),其以指定的模式來創(chuàng)造對(duì)象。
構(gòu)造函數(shù)
除了以指定模式創(chuàng)建對(duì)象之外,構(gòu)造函數(shù)也做了另一個(gè)有用的事情-它自動(dòng)地為新創(chuàng)建的對(duì)象設(shè)置一個(gè)原型對(duì)象。這個(gè)原型對(duì)象存儲(chǔ)在ConstructorFunction.prototype屬性中。
換句話說,我們可以使用構(gòu)造函數(shù)來重寫上一個(gè)擁有對(duì)象b和對(duì)象c的例子。因此,對(duì)象a(一個(gè)原型對(duì)象)的角色由Foo.prototype來扮演:
// a constructor function
function Foo(y) {
// which may create objects
// by specified pattern: they have after
// creation own "y" property
this.y = y;
}
// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:
// inherited property "x"
Foo.prototype.x = 10;
// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
// let's show that we reference
// properties we expect
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// also "Foo.prototype" automatically creates
// a special property "constructor", which is a
// reference to the constructor function itself;
// instances "b" and "c" may found it via
// delegation and use to check their constructor
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
);
這個(gè)代碼可以表示為如下關(guān)系:
這張圖又一次說明了每個(gè)對(duì)象都有一個(gè)原型。構(gòu)造函數(shù)Foo也有自己的__proto__,值為Function.prototype,F(xiàn)unction.prototype也通過其__proto__屬性關(guān)聯(lián)到Object.prototype。因此,重申一下,F(xiàn)oo.prototype就是Foo的一個(gè)明確的屬性,指向?qū)ο骲和對(duì)象c的原型。
正式來說,如果思考一下分類的概念(并且我們已經(jīng)對(duì)Foo進(jìn)行了分類),那么構(gòu)造函數(shù)和原型對(duì)象合在一起可以叫作「類」。實(shí)際上,舉個(gè)例子,Python的第一級(jí)(first-class)動(dòng)態(tài)類(dynamic classes)顯然是以同樣的屬性/方法處理方案來實(shí)現(xiàn)的。從這個(gè)角度來說,Python中的類就是ECMAScript使用的委托繼承的一個(gè)語法糖。
注意: 在ES6中「類」的概念被標(biāo)準(zhǔn)化了,并且實(shí)際上以一種構(gòu)建在構(gòu)造函數(shù)上面的語法糖來實(shí)現(xiàn),就像上面描述的一樣。從這個(gè)角度來看原型鏈成為了類繼承的一種具體實(shí)現(xiàn)方式:
// 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)這個(gè)主題的完整、詳細(xì)的解釋可以在ES3系列的第七章找到。分為兩個(gè)部分:7.1 面向?qū)ο?基本理論,在那里你將會(huì)找到對(duì)各種面向?qū)ο蠓独L(fēng)格的描述以及它們和ECMAScript之間的對(duì)比,然后在7.2 面向?qū)ο?ECMAScript實(shí)現(xiàn),是對(duì)ECMAScript中面向?qū)ο蟮慕榻B。
現(xiàn)在,在我們知道了對(duì)象的基礎(chǔ)之后,讓我們看看運(yùn)行時(shí)程序的執(zhí)行(runtime program execution)在ECMAScript中是如何實(shí)現(xiàn)的。這叫作執(zhí)行上下文棧(execution context stack),其中的每個(gè)元素也可以抽象成為一個(gè)對(duì)象。是的,ECMAScript幾乎在任何地方都和對(duì)象的概念打交道;)
執(zhí)行上下文堆棧
這里有三種類型的ECMAScript代碼:全局代碼、函數(shù)代碼和eval代碼。每個(gè)代碼是在其執(zhí)行上下文(execution context)中被求值的。這里只有一個(gè)全局上下文,可能有多個(gè)函數(shù)執(zhí)行上下文以及eval執(zhí)行上下文。對(duì)一個(gè)函數(shù)的每次調(diào)用,會(huì)進(jìn)入到函數(shù)執(zhí)行上下文中,并對(duì)函數(shù)代碼類型進(jìn)行求值。每次對(duì)eval函數(shù)進(jìn)行調(diào)用,會(huì)進(jìn)入eval執(zhí)行上下文并對(duì)其代碼進(jìn)行求值。
注意,一個(gè)函數(shù)可能會(huì)創(chuàng)建無數(shù)的上下文,因?yàn)閷?duì)函數(shù)的每次調(diào)用(即使這個(gè)函數(shù)遞歸的調(diào)用自己)都會(huì)生成一個(gè)具有新狀態(tài)的上下文:
function foo(bar) {}
// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)
foo(10);
foo(20);
foo(30);
一個(gè)執(zhí)行上下文可能會(huì)觸發(fā)另一個(gè)上下文,比如,一個(gè)函數(shù)調(diào)用另一個(gè)函數(shù)(或者在全局上下文中調(diào)用一個(gè)全局函數(shù)),等等。從邏輯上來說,這是以棧的形式實(shí)現(xiàn)的,它叫作執(zhí)行上下文棧。
一個(gè)觸發(fā)其他上下文的上下文叫作caller。被觸發(fā)的上下文叫作callee。callee在同一時(shí)間可能是一些其他callee的caller(比如,一個(gè)在全局上下文中被調(diào)用的函數(shù),之后調(diào)用了一些內(nèi)部函數(shù))。
當(dāng)一個(gè)caller觸發(fā)(調(diào)用)了一個(gè)callee,這個(gè)caller會(huì)暫緩自身的執(zhí)行,然后把控制權(quán)傳遞給callee。這個(gè)callee被push到棧中,并成為一個(gè)運(yùn)行中(活動(dòng)的)執(zhí)行上下文。在callee的上下文結(jié)束后,它會(huì)把控制權(quán)返回給caller,然后caller的上下文繼續(xù)執(zhí)行(它可能觸發(fā)其他上下文)直到它結(jié)束,以此類推。callee可能簡單的返回或者由于異常而退出。一個(gè)拋出的但是沒有被捕獲的異常可能退出(從棧中pop)一個(gè)或者多個(gè)上下文。
換句話說,所有ECMAScript程序的運(yùn)行時(shí)可以用執(zhí)行上下文(EC)棧來表示,棧頂是當(dāng)前活躍(active)上下文:
當(dāng)程序開始的時(shí)候它會(huì)進(jìn)入全局執(zhí)行上下文,此上下文位于棧底并且是棧中的第一個(gè)元素。然后全局代碼進(jìn)行一些初始化,創(chuàng)建需要的對(duì)象和函數(shù)。在全局上下文的執(zhí)行過程中,它的代碼可能觸發(fā)其他(已經(jīng)創(chuàng)建完成的)函數(shù),這些函數(shù)將會(huì)進(jìn)入它們自己的執(zhí)行上下文,向棧中push新的元素,以此類推。當(dāng)初始化完成之后,運(yùn)行時(shí)系統(tǒng)(runtime system)就會(huì)等待一些事件(比如,用戶鼠標(biāo)點(diǎn)擊),這些事件將會(huì)觸發(fā)一些函數(shù),從而進(jìn)入新的執(zhí)行上下文中。
在下個(gè)圖中,擁有一些函數(shù)上下文EC1和全局上下文Global EC,當(dāng)EC1進(jìn)入和退出全局上下文的時(shí)候下面的棧將會(huì)發(fā)生變化:
這就是ECMAScript的運(yùn)行時(shí)系統(tǒng)如何真正地管理代碼執(zhí)行的。
更多有關(guān)ECMAScript中執(zhí)行上下文的信息可以在對(duì)應(yīng)的第一章 執(zhí)行上下文中獲取。
像我們所說的,棧中的每個(gè)執(zhí)行上下文都可以用一個(gè)對(duì)象來表示。讓我們來看看它的結(jié)構(gòu)以及一個(gè)上下文到底需要什么狀態(tài)(什么屬性)來執(zhí)行它的代碼。
執(zhí)行上下文
一個(gè)執(zhí)行上下文可以抽象的表示為一個(gè)簡單的對(duì)象。每一個(gè)執(zhí)行上下文擁有一些屬性(可以叫作上下文狀態(tài))用來跟蹤和它相關(guān)的代碼的執(zhí)行過程。在下圖中展示了一個(gè)上下文的結(jié)構(gòu):
除了這三個(gè)必需的屬性(一個(gè)變量對(duì)象(variable objec),一個(gè)this值以及一個(gè)作用域鏈(scope chain))之外,執(zhí)行上下文可以擁有任何附加的狀態(tài),這取決于實(shí)現(xiàn)。
讓我們詳細(xì)看看上下文中的這些重要的屬性。
變量對(duì)象
變量對(duì)象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域。它是一個(gè)與上下文相關(guān)的特殊對(duì)象,其中存儲(chǔ)了在上下文中定義的變量和函數(shù)聲明。
注意,函數(shù)表達(dá)式(與函數(shù)聲明相對(duì))不包含在變量對(duì)象之中。
變量對(duì)象是一個(gè)抽象概念。對(duì)于不同的上下文類型,在物理上,是使用不同的對(duì)象。比如,在全局上下文中變量對(duì)象就是全局對(duì)象本身(這就是為什么我們可以通過全局對(duì)象的屬性名來關(guān)聯(lián)全局變量)。
讓我們在全局執(zhí)行上下文中考慮下面這個(gè)例子:
var foo = 10;
function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true
);
console.log(baz); // ReferenceError, "baz" is not defined
之后,全局上下文的變量對(duì)象(variable objec,簡稱VO)將會(huì)擁有如下屬性:
再看一遍,函數(shù)baz是一個(gè)函數(shù)表達(dá)式,沒有被包含在變量對(duì)象之中。這就是為什么當(dāng)我們想要在函數(shù)自身之外訪問它的時(shí)候會(huì)出現(xiàn)ReferenceError。
注意,與其他語言(比如C/C++)相比,在ECMAScript中只有函數(shù)可以創(chuàng)建一個(gè)新的作用域。在函數(shù)作用域中所定義的變量和內(nèi)部函數(shù)在函數(shù)外邊是不能直接訪問到的,而且并不會(huì)污染全局變量對(duì)象。
使用eval我們也會(huì)進(jìn)入一個(gè)新的(eval類型)執(zhí)行上下文。無論如何,eval使用全局的變量對(duì)象或者使用caller(比如eval被調(diào)用時(shí)所在的函數(shù))的變量對(duì)象。
那么函數(shù)和它的變量對(duì)象是怎么樣的?在函數(shù)上下文中,變量對(duì)象是以活動(dòng)對(duì)象(activation object)來表示的。
活動(dòng)對(duì)象
當(dāng)一個(gè)函數(shù)被caller所觸發(fā)(被調(diào)用),一個(gè)特殊的對(duì)象,叫作活動(dòng)對(duì)象(activation object)將會(huì)被創(chuàng)建。這個(gè)對(duì)象中包含形參和那個(gè)特殊的arguments對(duì)象(是對(duì)形參的一個(gè)映射,但是值是通過索引來獲取)。活動(dòng)對(duì)象之后會(huì)做為函數(shù)上下文的變量對(duì)象來使用。
換句話說,函數(shù)的變量對(duì)象也是一個(gè)同樣簡單的變量對(duì)象,但是除了變量和函數(shù)聲明之外,它還存儲(chǔ)了形參和arguments對(duì)象,并叫作活動(dòng)對(duì)象。
考慮如下例子:
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE
}
foo(10, 20);
我們看下函數(shù)foo的上下文中的活動(dòng)對(duì)象(activation object,簡稱AO):
并且函數(shù)表達(dá)式baz還是沒有被包含在變量/活動(dòng)對(duì)象中。
關(guān)于這個(gè)主題所有細(xì)節(jié)方面(像變量和函數(shù)聲明的提升問題(hoisting))的完整描述可以在同名的章節(jié)第二章 變量對(duì)象中找到。
注意,在ES5中變量對(duì)象和活動(dòng)對(duì)象被并入了詞法環(huán)境模型(lexical environments model),詳細(xì)的描述可以在對(duì)應(yīng)的章節(jié)找到。
然后我們向下一個(gè)部分前進(jìn)。眾所周知,在ECMAScript中我們可以使用內(nèi)部函數(shù),然后在這些內(nèi)部函數(shù)我們可以引用父函數(shù)的變量或者全局上下文中的變量。當(dāng)我們把變量對(duì)象命名為上下文的作用域?qū)ο?/i>,與上面討論的原型鏈相似,這里有一個(gè)叫作作用域鏈的東西。
作用域鏈
作用域鏈?zhǔn)且粋€(gè)對(duì)象列表,上下文代碼中出現(xiàn)的標(biāo)識(shí)符在這個(gè)列表中進(jìn)行查找。
這個(gè)規(guī)則還是與原型鏈同樣簡單以及相似:如果一個(gè)變量在函數(shù)自身的作用域(在自身的變量/活動(dòng)對(duì)象)中沒有找到,那么將會(huì)查找它父函數(shù)(外層函數(shù))的變量對(duì)象,以此類推。
就上下文而言,標(biāo)識(shí)符指的是:變量名稱,函數(shù)聲明,形參,等等。當(dāng)一個(gè)函數(shù)在其代碼中引用一個(gè)不是局部變量(或者局部函數(shù)或者一個(gè)形參)的標(biāo)識(shí)符,那么這個(gè)標(biāo)識(shí)符就叫作自由變量。搜索這些自由變量(free variables)正好就要用到作用域鏈。
在通常情況下,作用域鏈是一個(gè)包含所有父(函數(shù))變量對(duì)象__加上(在作用域鏈頭部的)函數(shù)自身變量/活動(dòng)對(duì)象的一個(gè)列表。但是,這個(gè)作用域鏈也可以包含任何其他對(duì)象,比如,在上下文執(zhí)行過程中動(dòng)態(tài)加入到作用域鏈中的對(duì)象-像with對(duì)象或者特殊的catch從句(catch-clauses)對(duì)象。
當(dāng)解析(查找)一個(gè)標(biāo)識(shí)符的時(shí)候,會(huì)從作用域鏈中的活動(dòng)對(duì)象開始查找,然后(如果這個(gè)標(biāo)識(shí)符在函數(shù)自身的活動(dòng)對(duì)象中沒有被查找到)向作用域鏈的上一層查找-重復(fù)這個(gè)過程,就和原型鏈一樣。
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();
})();
我們可以假設(shè)通過隱式的__parent__屬性來和作用域鏈對(duì)象進(jìn)行關(guān)聯(lián),這個(gè)屬性指向作用域鏈中的下一個(gè)對(duì)象。這個(gè)方案可能在真實(shí)的Rhino代碼中經(jīng)過了測試,并且這個(gè)技術(shù)很明確得被用于ES5的詞法環(huán)境中(在那里被叫作outer連接)。作用域鏈的另一個(gè)表現(xiàn)方式可以是一個(gè)簡單的數(shù)組。利用__parent__概念,我們可以用下面的圖來表現(xiàn)上面的例子(并且父變量對(duì)象存儲(chǔ)在函數(shù)的[[Scope]]屬性中):
在代碼執(zhí)行過程中,作用域鏈可以通過使用with語句和catch從句對(duì)象來增強(qiáng)。并且由于這些對(duì)象是簡單的對(duì)象,它們可以擁有原型(和原型鏈)。這個(gè)事實(shí)導(dǎo)致作用域鏈查找變?yōu)?i>兩個(gè)維度:(1)首先是作用域鏈連接,然后(2)在每個(gè)作用域鏈連接上-深入作用域鏈連接的原型鏈(如果此連接擁有原型)。
對(duì)于這個(gè)例子:
Object.prototype.x = 10;
var w = 20;
var y = 30;
// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain
console.log(x); // 10
(function foo() {
// "foo" local variables
var w = 40;
var x = 100;
// "x" is found in the
// "Object.prototype", because
// {z: 50} inherits from it
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// after "with" object is removed
// from the scope chain, "x" is
// again found in the AO of "foo" context;
// variable "w" is also local
console.log(x, w); // 100, 40
// and that's how we may refer
// shadowed global "w" variable in
// the browser host environment
console.log(window.w); // 20
})();
我們可以給出如下的結(jié)構(gòu)(確切的說,在我們查找__parent__連接之前,首先查找__proto__鏈):
注意,不是在所有的實(shí)現(xiàn)中全局對(duì)象都是繼承自O(shè)bject.prototype。上圖中描述的行為(從全局上下文中引用「未定義」的變量x)可以在諸如SpiderMonkey引擎中進(jìn)行測試。
由于所有父變量對(duì)象都存在,所以在內(nèi)部函數(shù)中獲取父函數(shù)中的數(shù)據(jù)沒有什么特別-我們就是遍歷作用域鏈去解析(搜尋)需要的變量。就像我們上邊提及的,在一個(gè)上下文結(jié)束之后,它所有的狀態(tài)和它自身都會(huì)被銷毀。在同一時(shí)間父函數(shù)可能會(huì)返回一個(gè)內(nèi)部函數(shù)。而且,這個(gè)返回的函數(shù)之后可能在另一個(gè)上下文中被調(diào)用。如果自由變量的上下文已經(jīng)「消失」了,那么這樣的調(diào)用將會(huì)發(fā)生什么?通常來說,有一個(gè)概念可以幫助我們解決這個(gè)問題,叫作(詞法)閉包,其在ECMAScript中就是和作用域鏈的概念緊密相關(guān)的。
閉包
在ECMAScript中,函數(shù)是第一級(jí)(first-class)對(duì)象。這個(gè)術(shù)語意味著函數(shù)可以做為參數(shù)傳遞給其他函數(shù)(在那種情況下,這些參數(shù)叫作「函數(shù)類型參數(shù)」(funargs,是"functional arguments"的簡稱))。接收「函數(shù)類型參數(shù)」的函數(shù)叫作高階函數(shù)或者,貼近數(shù)學(xué)一些,叫作高階操作符。同樣函數(shù)也可以從其他函數(shù)中返回。返回其他函數(shù)的函數(shù)叫作以函數(shù)為值(function valued)的函數(shù)(或者叫作擁有函數(shù)類值的函數(shù)(functions with functional value))。
這有兩個(gè)在概念上與「函數(shù)類型參數(shù)(funargs)」和「函數(shù)類型值(functional values)」相關(guān)的問題。并且這兩個(gè)子問題在"Funarg problem"(或者叫作"functional argument"問題)中很普遍。為了解決整個(gè)"funarg problem",閉包(closure)的概念被創(chuàng)造了出來。我們詳細(xì)的描述一下這兩個(gè)子問題(我們將會(huì)看到這兩個(gè)問題在ECMAScript中都是使用圖中所提到的函數(shù)的[[Scope]]屬性來解決的)。
「funarg問題」的第一個(gè)子問題是「向上funarg問題」(upward funarg problem)。它會(huì)在當(dāng)一個(gè)函數(shù)從另一個(gè)函數(shù)向上返回(到外層)并且使用上面所提到的自由變量的時(shí)候出現(xiàn)。為了在即使父函數(shù)上下文結(jié)束的情況下也能訪問其中的變量,內(nèi)部函數(shù)在被創(chuàng)建的時(shí)候會(huì)在它的[[Scope]]屬性中保存父函數(shù)的作用域鏈。所以當(dāng)函數(shù)被調(diào)用的時(shí)候,它上下文的作用域鏈會(huì)被格式化成活動(dòng)對(duì)象與[[Scope]]屬性的和(實(shí)際上就是我們剛剛在上圖中所看到的):
Scope chain = Activation object + [[Scope]]
再次注意這個(gè)關(guān)鍵點(diǎn)-確切的說在創(chuàng)建時(shí)刻-函數(shù)會(huì)保存父函數(shù)的作用域鏈,因?yàn)榇_切的說這個(gè)保存下來的作用域鏈將會(huì)在未來的函數(shù)調(diào)用時(shí)用來查找變量。
function foo() {
var x = 10;
return function bar() {
console.log(x);
};
}
// "foo" returns also a function
// and this returned function uses
// free variable "x"
var returnedFunction = foo();
// global variable "x"
var x = 20;
// execution of the returned function
returnedFunction(); // 10, but not 20
這個(gè)類型的作用域叫作靜態(tài)(或者詞法)作用域。我們看到變量x在返回的bar函數(shù)的[[Scope]]屬性中被找到。通常來說,也存在動(dòng)態(tài)作用域,那么上面例子中的變量x將會(huì)被解析成20,而不是10。但是,動(dòng)態(tài)作用域在ECMAScript中沒有被使用。
「funarg問題」的第二個(gè)部分是「向下funarg問題」。這種情況下可能會(huì)存在一個(gè)父上下文,但是在解析標(biāo)識(shí)符的時(shí)候可能會(huì)模糊不清。問題是:標(biāo)識(shí)符該使用哪個(gè)作用域的值-以靜態(tài)的方式存儲(chǔ)在函數(shù)創(chuàng)建時(shí)刻的還是在執(zhí)行過程中以動(dòng)態(tài)方式生成的(比如caller的作用域)?為了避免這種模棱兩可的情況并形成閉包,靜態(tài)作用域被采用:
// global "x"
var x = 10;
// global function
function foo() {
console.log(x);
}
(function (funArg) {
// local "x"
var x = 20;
// there is no ambiguity,
// because we use global "x",
// which was statically saved in
// [[Scope]] of the "foo" function,
// but not the "x" of the caller's scope,
// which activates the "funArg"
funArg(); // 10, but not 20
})(foo); // pass "down" foo as a "funarg"
我們可以斷定靜態(tài)作用域是一門語言擁有閉包的必需條件。但是,一些語言可能會(huì)同時(shí)提供動(dòng)態(tài)和靜態(tài)作用域,允許程序員做選擇-什么應(yīng)該包含(closure)在內(nèi)和什么不應(yīng)包含在內(nèi)。由于在ECMAScript中只使用了靜態(tài)作用域(比如我們對(duì)于funarg問題的兩個(gè)子問題都有解決方案),所以結(jié)論是:ECMAScript完全支持閉包,技術(shù)上是通過函數(shù)的[[Scope]]屬性實(shí)現(xiàn)的。現(xiàn)在我們可以給閉包下一個(gè)準(zhǔn)確的定義:
閉包是一個(gè)代碼塊(在ECMAScript是一個(gè)函數(shù))和以靜態(tài)方式/詞法方式進(jìn)行存儲(chǔ)的所有父作用域的一個(gè)集合體。所以,通過這些存儲(chǔ)的作用域,函數(shù)可以很容易的找到自由變量。
注意,由于每個(gè)(標(biāo)準(zhǔn)的)函數(shù)都在創(chuàng)建的時(shí)候保存了[[Scope]],所以理論上來講,ECMAScript中的所有函數(shù)都是閉包。
另一個(gè)需要注意的重要事情是,多個(gè)函數(shù)可能擁有相同的父作用域(這是很常見的情況,比如當(dāng)我們擁有兩個(gè)內(nèi)部/全局函數(shù)的時(shí)候)。在這種情況下,[[Scope]]屬性中存儲(chǔ)的變量是在擁有相同父作用域鏈的所有函數(shù)之間共享的。一個(gè)閉包對(duì)變量進(jìn)行的修改會(huì)體現(xiàn)在另一個(gè)閉包對(duì)這些變量的讀取上:
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
);
以上代碼可以通過下圖進(jìn)行說明:
確切來說這個(gè)特性在循環(huán)中創(chuàng)建多個(gè)函數(shù)的時(shí)候會(huì)使人非常困惑。在創(chuàng)建的函數(shù)中使用循環(huán)計(jì)數(shù)器的時(shí)候,一些程序員經(jīng)常會(huì)得到非預(yù)期的結(jié)果,所有函數(shù)中的計(jì)數(shù)器都是同樣的值。現(xiàn)在是到了該揭開謎底的時(shí)候了-因?yàn)樗羞@些函數(shù)擁有同一個(gè)[[Scope]],這個(gè)屬性中的循環(huán)計(jì)數(shù)器的值是最后一次所賦的值。
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è)額外的對(duì)象-比如,使用額外函數(shù):
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
alert(x);
};
})(k); // pass "k" value
}
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
對(duì)閉包理論和它們的實(shí)際應(yīng)用感興趣的同學(xué)可以在第六章 閉包中找到額外的信息。如果想獲取更多關(guān)于作用域鏈的信息,可以看一下同名的第四章 作用域鏈。
然后我們移動(dòng)到下個(gè)部分,考慮一下執(zhí)行上下文的最后一個(gè)屬性。這就是關(guān)于this值的概念。
This
this是一個(gè)與執(zhí)行上下文相關(guān)的特殊對(duì)象。因此,它可以叫作上下文對(duì)象(也就是用來指明執(zhí)行上下文是在哪個(gè)上下文中被觸發(fā)的對(duì)象)。
任何對(duì)象都可以做為上下文中的this的值。我想再一次澄清,在一些對(duì)ECMAScript執(zhí)行上下文和部分this的描述中的所產(chǎn)生誤解。this經(jīng)常被錯(cuò)誤的描述成是變量對(duì)象的一個(gè)屬性。這類錯(cuò)誤存在于比如像這本書中(即使如此,這本書的相關(guān)章節(jié)還是十分不錯(cuò)的)。再重復(fù)一次:
this是執(zhí)行上下文的一個(gè)屬性,而不是變量對(duì)象的一個(gè)屬性
這個(gè)特性非常重要,因?yàn)?i>與變量相反,this從不會(huì)參與到標(biāo)識(shí)符解析過程。換句話說,在代碼中當(dāng)訪問this的時(shí)候,它的值是直接從執(zhí)行上下文中獲取的,并不需要任何作用域鏈查找。this的值只在進(jìn)入上下文的時(shí)候進(jìn)行一次確定。
順便說一下,與ECMAScript相反,比如,Python的方法都會(huì)擁有一個(gè)被當(dāng)作簡單變量的self參數(shù),這個(gè)變量的值在各個(gè)方法中是相同的的并且在執(zhí)行過程中可以被更改成其他值。在ECMAScript中,給this賦一個(gè)新值是不可能的,因?yàn)椋僦貜?fù)一遍,它不是一個(gè)變量并且不存在于變量對(duì)象中。
在全局上下文中,this就等于全局對(duì)象本身(這意味著,這里的this等于變量對(duì)象):
var x = 10;
console.log(
x, // 10
this.x, // 10
window.x // 10
);
在函數(shù)上下文的情況下,對(duì)函數(shù)的每次調(diào)用,其中的this值可能是不同的。這個(gè)this值是通過函數(shù)調(diào)用表達(dá)式(也就是函數(shù)被調(diào)用的方式)的形式由caller所提供的。舉個(gè)例子,下面的函數(shù)foo是一個(gè)callee,在全局上下文中被調(diào)用,此上下文為caller。讓我們通過例子看一下,對(duì)于一個(gè)代碼相同的函數(shù),this值是如何在不同的調(diào)用中(函數(shù)觸發(fā)的不同方式),由caller給出不同的結(jié)果的:
// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation
function foo() {
alert(this);
}
// caller activates "foo" (callee) and
// provides "this" for the callee
foo(); // global object
foo.prototype.constructor(); // foo.prototype
var bar = {
baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object
var otherFoo = bar.baz;
otherFoo(); // again global object
為了深入理解this為什么(并且更本質(zhì)一些-如何)在每個(gè)函數(shù)調(diào)用中可能會(huì)發(fā)生變化,你可以閱讀第三章 This。在那里,上面所提到的情況都會(huì)有詳細(xì)的討論。
總結(jié)
通過本文我們完成了對(duì)概要的綜述。盡管,它看起來并不像是「概要」;)。對(duì)所有這些主題進(jìn)行完全的解釋需要一本完整的書。我們只是沒有涉及到兩個(gè)大的主題:函數(shù)(和不同函數(shù)之間的區(qū)別,比如,函數(shù)聲明和函數(shù)表達(dá)式)和ECMAScript中所使用的求值策略(evaluation strategy )。這兩個(gè)主題是可以ES3系列的在對(duì)應(yīng)章節(jié)找到:第五章 函數(shù)和第八章 求值策略。