我們首先來(lái)看一下對(duì)象[Object]的概念,這也是 ECMASript 中最基本的概念。
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)題。
原型對(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ì)象。
除了創(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 的身影。
在 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) 。
一個(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è)屬性。
注意:函數(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)函數(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]。
作用域鏈?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)搜尋。
我們?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)。
在 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適合執(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ì)討論。
在此我們完成了一個(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)力。