前言
本文翻譯自scope-chai
概要
通過第變量對象的學(xué)習(xí)我們知道蚊夫,執(zhí)行上下文的數(shù)據(jù)(變量、函數(shù)聲明、函數(shù)形參)都是以屬性的方式儲存在變量對象中
我們還知道燕酷,變量對象是在進(jìn)入執(zhí)行上下文階段被創(chuàng)建和初始化查库,隨后在執(zhí)行代碼階段會對屬性值進(jìn)行更新
本文將深入討論與執(zhí)行上下文密切相關(guān)的另外一個重要的概念 —— 作用域鏈(Scope Chain)
定義
如果簡單扼要地講路媚,那么作用域鏈就是與內(nèi)部函數(shù)息息相關(guān)的一個概念
眾所周知,ECMAScript
允許創(chuàng)建內(nèi)部函數(shù)樊销,甚至可以將這些內(nèi)部函數(shù)作為父函數(shù)的返回值
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
每個上下文都有自己的變量對象整慎;對于全局變量脏款,其變量對象就是全局對象自己本身;對于函數(shù)而言裤园,其變量對象就是活動對象
作用域鏈是所以內(nèi)部上下文和變量對象的列表撤师,用于變量查詢。比如拧揽,在上述例子中丈氓,bar上下文的作用域鏈包含了AO(bar)、AO(foo)强法、VO(global)
作用域鏈?zhǔn)且粭l變量對應(yīng)的鏈万俗,它和執(zhí)行上下文有關(guān),用于處理標(biāo)識符時候進(jìn)行變量查詢
作用域鏈在函數(shù)調(diào)用時被創(chuàng)建饮怯,它包含了活動對象(AO)和該函數(shù)的內(nèi)部屬性[[scope]]
.關(guān)于[[scope]]
會在后面做詳細(xì)介紹
activeExecutionContext = {
VO: {...}, // 或者 AO
this: thisValue,
Scope: [ // 作用域鏈
// 所有變量對象的列表
// 用于標(biāo)識符查找
]
};
上述代碼中Scope
定義如下:
Scope = AO + [[Scope]]
針對我們的例子闰歪,我們可以將Scope
和[[scope]]
用普通的ECMAScript數(shù)組來表示:
var Scope = [VO1, VO2, ...., VOn] //作用域鏈
除此之外,還可以用多級的對象鏈的數(shù)據(jù)結(jié)構(gòu)來表示蓖墅,鏈中每一個鏈接都有對父作用域(上層變量對象)的引用
var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->
然而库倘,使用數(shù)組來表示作用域鏈會更方便,因此论矾,我們這里就采用數(shù)組的表示方式教翩。 除此之外,不論在實現(xiàn)層是否采用包含__parent__
特性的分層對象鏈的數(shù)據(jù)結(jié)構(gòu)贪壳,規(guī)范對其做了抽象的定義“作用域鏈?zhǔn)且粋€對象列表”饱亿。數(shù)組就是實現(xiàn)列表這一概念最好的選擇。
下面將要介紹的AO+[[Scope]]
以及標(biāo)識符的處理方式闰靴,都和函數(shù)的生命周期有關(guān)彪笼。
函數(shù)生命周期
函數(shù)的生命分為創(chuàng)建和激活(調(diào)用)階段,下面分別詳細(xì)介紹
創(chuàng)建階段
我們知道蚂且,進(jìn)入上下文階段時函數(shù)聲明被儲存在變量對象/活動對象中(VO/AO)配猫。讓我們看看在全局上下文中的變量和函數(shù)聲明的例子(這里變量對象是全局對象自身,還記得杏死,是吧泵肄?)
var x = 10;
function foo() {
var y = 20;
alert(x + y);
}
foo(); // 30
在函數(shù)激活(調(diào)用)后,我們得到了正確(預(yù)期)的結(jié)果——30淑翼。不過腐巢,這里有個非常重要的特性
此前,我們僅僅談到當(dāng)前上下文的變量對象窒舟。這里系忙,變量y
在函數(shù)foo
中定義(意味著它在foo
上下文的AO
中),但是變量x
并未在foo
上下文中定義,自然不會被添加到foo
的AO中惠豺。乍一看银还,變量 x 相對于函數(shù) foo 根本就不存在风宁。
fooContext.AO = {
y: undefined // undefined – 在進(jìn)入上下文時, 20 – 在激活階段
};
那么,foo函數(shù)是如何訪問到x
變量的蛹疯?一個順其自然的想法是:函數(shù)應(yīng)當(dāng)有訪問更高層上下文變量對象的權(quán)限戒财。而事實也恰是如此,就是通過函數(shù)的內(nèi)部屬性 [[Scope]]來實現(xiàn)這一機制的捺弦。
[[Scope]] 是一個包含了所有上層變量對象的分層鏈饮寞,它屬于當(dāng)前函數(shù)上下文,并在函數(shù)創(chuàng)建的時候列吼,保存在函數(shù)中幽崩。
這里要注意的很重要的一點是:[[Scope]]是在函數(shù)創(chuàng)建的時候保存起來的——靜態(tài)的(不變的),永遠(yuǎn)永遠(yuǎn)——直到函數(shù)銷毀寞钥。也就是說慌申,哪怕函數(shù)永遠(yuǎn)都不能被調(diào)用到,[[Scope]]屬性也已經(jīng)保存在函數(shù)對象上了
另外要注意的一點是:[[Scope]] 與 Scope (作用域鏈)是不同的理郑,前者是函數(shù)的屬性蹄溉,后者是上下文的屬性。 以上述例子來說您炉,foo 函數(shù)的 [[Scope]] 如下所示:
foo.[[Scope]] = [
globalContext.VO // === Global
];
當(dāng)函數(shù)被調(diào)用的時候柒爵,就進(jìn)入函數(shù)執(zhí)行上下文,此時活動對象唄創(chuàng)建赚爵,this
和作用域(作用域鏈
被確定棉胀。下面我們詳細(xì)討論這個時刻。
激活階段
正如上面定義的那樣囱晴,在進(jìn)入上下文膏蚓,AO/VO 創(chuàng)建之后,上下文的Scope 屬性(作用域鏈畸写,用于變量查詢)會定義為如下所示:
Scope = AO|VO + [[Scope]]
特別注意的是活動對象是Scope數(shù)組元素的第一個元素,添加在作用域的最前端
Scope = [AO].concat([[Scope]]);
這個特性對處理標(biāo)識符非常重要
處理標(biāo)識符其實就是一個確定變量(或者函數(shù)聲明)屬于作用域鏈中哪個變量對象的過程氓扛。
此算法返回的總是一個引用類型的值枯芬,其base
屬性就是對應(yīng)的變量對象(或者變量對象不存在的時候則返回null),其propertyname
屬性的名字就是要查詢的標(biāo)識符采郎。
標(biāo)識符處理過程包括了對應(yīng)的變量名的屬性查詢千所,即在作用域鏈中會進(jìn)行一系列的變量對象的檢測,從作用域鏈的最底層上下文一直到最上層上下文
因此蒜埋,在查詢過程中上下文中的局部變量比上層上下文的變量會優(yōu)先被查詢到淫痰,換句話說,如果兩個相同名字的變量存在于不同的上下文中時整份,處于底層上下文的變量會優(yōu)先被找到
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
全局上下文的變量對象如下所示:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
全局上下文的變量對象如下所示:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在 foo 函數(shù)創(chuàng)建的時候待错,其 [[Scope]] 屬性如下所示:
foo.[[Scope]] = [
globalContext.VO
];
在 foo 函數(shù)激活的時候(進(jìn)入上下文時)籽孙,foo 函數(shù)上下文的活躍對象如下所示:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
同時,foo 函數(shù)上下文的作用域鏈如下所示:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
在內(nèi)部bar
函數(shù)創(chuàng)建的時候火俄,其 [[Scope]] 屬性如下所示:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在 bar 函數(shù)激活的時候犯建,其對應(yīng)的活躍對象如下所示:
barContext.AO = {
z: 30
};
同時,bar 函數(shù)上下文的作用域鏈如下所示:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
如下是 x瓜客,y 和 z 標(biāo)識符的查詢過程:
- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10
- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20
- "z"
-- barContext.AO // found - 30
作用域的特性
下面讓我們看看與作用域鏈和函數(shù)[[scope]]屬性相關(guān)的一些重要特征适瓦。
閉包
在 ECMAScript 中,閉包和函數(shù)的[[Scope]] 屬性息息相關(guān)谱仪。正如此前介紹的玻熙,[[Scope]]是在函數(shù)創(chuàng)建的時候就保存在函數(shù)對象上了,并且直到函數(shù)銷毀的時候才消失疯攒。事實上嗦随,閉包就是函數(shù)代碼和其 [[Scope]] 屬性的組合。因此卸例,[[Scope]] 包含了函數(shù)創(chuàng)建所在的詞法環(huán)境(上層變量對象)称杨。上層上下文中的變量,可以在函數(shù)激活的時候筷转,通過變量對象的詞法鏈(函數(shù)創(chuàng)建的時候就保存起來了)查詢到
var x = 10;
function foo() {
alert(x);
}
(function () {
var x = 20;
foo(); // 10, but not 20
})();
變量 x 是在 foo 函數(shù)的 [[Scope]] 中找到的姑原。對于變量查詢而言,詞法鏈?zhǔn)窃诤瘮?shù)創(chuàng)建的時候就定義的呜舒,而不是在調(diào)用函數(shù)時動態(tài)確定的(這個時候锭汛,變量 x 才會是 20)。
下面是另一個典型的閉包的例子:
function foo() {
var x = 10;
var y = 20;
return function () {
alert([x, y]);
};
}
var x = 30;
var bar = foo(); // 返回一個匿名函數(shù)
bar(); // [10, 20]
上述例子再一次證明了處理標(biāo)識符的時候袭蝗,詞法作用域鏈?zhǔn)窃诤瘮?shù)創(chuàng)建的時候定義的 —— 變量x的值是10唤殴,而不是30。并且到腥,上述例子清楚的展示了函數(shù)(上述例子中指的是函數(shù) foo 返回的匿名函數(shù))的[[Scope]] 屬性朵逝,即使在創(chuàng)建該函數(shù)的上下文結(jié)束的時候依然存在
通過 Function
構(gòu)造器創(chuàng)建的函數(shù)的 [[Scope]]屬性
**屬性,并且通過該屬性可以獲取所有上層上下文中的變量乡范。然而配名,這里有個例外,就是當(dāng)函數(shù)通過Function
構(gòu)造器創(chuàng)建的時候
var x = 10;
function foo() {
var y = 20;
function barFD() { // FunctionDeclaration
alert(x);
alert(y);
}
var barFE = function () { // FunctionExpression
alert(x);
alert(y);
};
var barFn = Function('alert(x); alert(y);');
barFD(); // 10, 20
barFE(); // 10, 20
barFn(); // 10, "y" is not defined
}
foo();
上述例子中晋辆,函數(shù)barFn
就是通過Fuction
構(gòu)造器來創(chuàng)建的渠脉,這個時候變量y 就無法訪問到了。但這并不意味著函數(shù)barFn
就沒有內(nèi)部的[[Scope]]屬性(否則它連變量 x 都無法訪問到)瓶佳。問題就在于當(dāng)函數(shù)通過Function
構(gòu)造器來創(chuàng)建的時候芋膘,其[[Scope]]屬性永遠(yuǎn)都只包含全局對象。哪怕在上層上下文中(非全局上下文)創(chuàng)建一個閉包都是無濟于事的
二維作用域鏈查找
在作用域鏈查找的時候還有很重要的一點:需要考慮變量對象的原型(如果存在的話) -- 源于原型鏈的特性:如果一個屬性在對象中沒有直接找到,查詢將在原型鏈中繼續(xù)为朋。即常說的二維鏈查找臂拓。(1)作用域鏈環(huán)節(jié);(2)每個作用域鏈 -- 深入到原型鏈環(huán)節(jié)潜腻。如果在 Object.prototype 中定義了屬性埃儿,我們能看到這種效果。
function foo() {
alert(x);
}
Object.prototype.x = 10;
foo(); // 10
活動對象是沒有原型的融涣,我們可以在下面的例子中看出:
function foo() {
var x = 20;
function bar() {
alert(x);
}
bar();
}
Object.prototype.x = 10;
foo(); // 20
試想下童番,如果 bar 函數(shù)的活動對象有原型的話,屬性 x 則應(yīng)當(dāng)在Object.prototype
中找到威鹿,因為它在 AO 中根本不存在剃斧。然而,上面第一個例子中忽你,在標(biāo)識符處理階段遍歷了整個作用域鏈幼东,到了全局對象(部分實現(xiàn)是這樣的),它繼承自 Object.prototype
科雳,因此根蟹,最終變量 x 的值就變成了 10。
執(zhí)行代碼階段對作用域的影響
在代碼執(zhí)行階段有兩個語句能修改作用域鏈糟秘,那就是 with 聲明和 catch 語句简逮。在標(biāo)識符查詢階段,這兩者都會被添加到作用域鏈的最前面尿赚。也就是說散庶,當(dāng)有 with 或 catch 的時候,作用域鏈就會被修改如下形式:
Scope = withObject|catchObject + AO|VO + [[Scope]]
如下例子中凌净,with 語句添加了 foo 對象悲龟,使得它的屬性可以不需要前綴直接訪問。
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
對應(yīng)的作用域鏈修改為如下所示:
Scope = foo + AO|VO + [[Scope]]
再看下面例子冰寻,with 對象被添加到作用域鏈的最前端:
var x = 10, y = 10;
with ({x: 20}) {
var x = 30, y = 30;
alert(x); // 30
alert(y); // 30
}
alert(x); // 10
alert(y); // 30
這里發(fā)生了什么须教?在進(jìn)入上下文階段,x和y被添加到變量對象中斩芭,在代碼執(zhí)行階段没卸,發(fā)生了如下修改:
x = 10, y = 10 {x: 20} 被添加到作用域鏈的最前端
在with內(nèi)部,遇到了var聲明秒旋,當(dāng)然什么也沒創(chuàng)建,因為在進(jìn)入上下文時诀拭,所有變量已被解析添加
這里只修改了x的值迁筛,此時的x被解析后是第二步中添加到作用域鏈最前的的那個對象中的 x,x的值由20變?yōu)?0
這里也修改了 y 的值,y 是上層作用域變量對象的屬性细卧,相應(yīng)地尉桩,由 10 修改為 30
當(dāng) with 語句結(jié)束后,這個特殊對象從作用域鏈中移除(被修改后的 x - 30 也隨著對象被移除了)贪庙,也就是說蜘犁,作用域鏈回到執(zhí)行 with 語句之前的狀態(tài)
正如在最后兩個 alert 中看到的,x 的值恢復(fù)到了原先的 10止邮,而 y 的值因為在 with 語句的時候被修改過了这橙,因此變?yōu)榱?30
同樣,catch 語句會創(chuàng)建一個只包含一個屬性(異常參數(shù)名)的新對象导披。如下所示:
try {
...
} catch (ex) {
alert(ex);
}
作用域鏈修改為:
var catchObject = {
ex:
};
Scope = catchObject + AO|VO + [[Scope]]
在 catch 從句結(jié)束后屈扎,作用域鏈同樣也會恢復(fù)到之前的狀態(tài)