在第二章變量對象的時候,已經介紹過執行上下文的數據是以變量對象的屬性的形式進行存儲的。算法
還介紹了,每次進入執行上下文的時候,就會建立變量對象,而且賦予其屬性初始值,隨後在執行代碼階段會對屬性值進行更新。數組
本文要與執行上下文密切相關的另一個重要的概念——做用域鏈(Scope Chain)。bash
衆所周知,ECMAScript容許建立內部函數,甚至能夠將這些內部函數做爲父函數的返回值。數據結構
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
複製代碼
每一個上下文都有本身的變量對象:對於全局上下文而言,其變量對象就是全局對象自己,對於函數而言,其變量對象就是活躍對象。閉包
做用域鏈其實就是全部內部上下文的變量對象的列表。用於變量查詢。好比,在上述例子中,「bar」上下文的做用域鏈包含了AO(bar),AO(foo)和VO(global)。ecmascript
下面就來詳細介紹下做用域鏈。ide
先從定義開始,隨後再結合例子詳細介紹:函數
做用域鏈是一條變量對象的鏈,它和執行上下文有關,用於在處理標識符時候進行變量查詢。
複製代碼
函數上下文的做用域鏈在函數調用的時候建立出來,它包含了活躍對象和該函數的內部[[Scope]]屬性。關於[[Scope]]會在後面做詳細介紹。post
大體表示以下:ui
activeExecutionContext = {
VO: {...}, // 或者 AO
this: thisValue,
Scope: [ // 所用域鏈
// 全部變量對象的列表
// 用於標識符查詢
]
};
複製代碼
上述代碼中的Scope定義爲以下所示:
Scope = AO + [[Scope]]
複製代碼
針對咱們的例子來講,能夠將Scope和[[Scope]]用普通的ECMAScript數組來表示:
var Scope = [VO1, VO2, ..., VOn]; // 做用域鏈
複製代碼
除此以外,還能夠用分層對象鏈的數據結構來表示,鏈中每個連接都有對父做用域(上層變量對象)的引用。這種表示方式和第二章中討論的某些實現中__parent__的概念相對應:
var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->
// etc.
複製代碼
然而,使用數組來表示做用域鏈會更方便,所以,咱們這裏就採用數組的表示方式。 除此以外,不論在實現層是否採用包含__parent__特性的分層對象鏈的數據結構,標準自身對其作了抽象的定義「做用域鏈是一個對象列表」。 數組就是實現列表這一律念最好的選擇。
下面將要介紹的 AO+[[Scope]]以及標識符的處理方式,都和函數的生命週期有關。
var x = 10;
function foo() {
var y = 20;
alert(x + y);
}
foo(); // 30
複製代碼
在函數激活後,咱們看到了正確(預期)的結果——30。不過,這裏有一個很是重要的特性。
在說當前上下文的變量對象前。上述代碼中咱們看到變量「y」是在「foo」函數中定義的(意味着它存儲在「foo」上下文的AO對象中), 然而變量「x」則並無在「foo」上下文中定義,天然也不會添加到「foo」的AO中。乍一眼看過去,變量「x」壓根就不在「foo」中存在; 然而,正如咱們下面要看到的——僅僅只是「乍一眼看過去「而已。咱們看到「foo」上下文的活躍對象中只包含一個屬性——「y」:
fooContext.AO = {
y: undefined // undefined – 在進入上下文時, 20 – 在激活階段
};
複製代碼
那麼,「foo」函數究竟是如何訪問到變量「x」的呢?一個順其天然的想法是:函數應當有訪問更高層上下文變量對象的權限。 而事實也恰是如此,就是經過函數的內部屬性[[Scope]]來實現這一機制的。
[[Scope]]是一個包含了全部上層變量對象的分層鏈,它屬於當前函數上下文,並在函數建立的時候,保存在函數中。
這裏要注意的很重要的一點是:[[Scope]]是在函數建立的時候保存起來的——靜態的(不變的),只有一次而且一直都存在——直到函數銷燬。 比方說,哪怕函數永遠都不能被調用到,[[Scope]]屬性也已經保存在函數對象上了。
另外要注意的一點是: [[Scope]]與Scope(做用域鏈)是不一樣的,前者是函數的屬性,後者是上下文的屬性。 以上述例子來講,「foo」函數的[[Scope]]以下所示:
foo.[[Scope]] = [
globalContext.VO // === Global
];
複製代碼
以後,有了函數調用,就會進入函數上下文,這個時候會建立活躍對象而且this的值和Scope(做用域鏈)都會肯定。下面來詳細介紹下。
Scope = AO|VO + [[Scope]]
複製代碼
這裏要注意的是活躍對象是Scope數組的第一個元素。添加在做用域鏈的最前面:
Scope = [AO].concat([[Scope]]);
複製代碼
此特性對處理標識符很是重要。
處理標識符其實就是一個肯定變量(或者函數聲明)屬於做用域鏈中哪一個變量對象的過程。
複製代碼
此算法返回的老是一個引用類型的值,其base屬性就是對應的變量對象(或者若是變量不存在的時候則返回null),其property name屬性的名字就是要查詢的標識符。 要詳細瞭解引用類型能夠參看第三章-this。
標識符處理過程包括了對應的變量名的屬性查詢,好比:在做用域鏈中會進行一系列的變量對象的檢測,從做用域鏈的最底層上下文一直到最上層上下文。
所以,在查詢過程當中上下文中的局部變量相比較上層上下文的變量會優先被查詢到,換句話說,若是兩個相同名字的變量存在於不一樣的上下文中時,處於底層上下文的變量會優先被找到。
下面是一個相對比較複雜的例子:
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
複製代碼
針對上述代碼,對應了以下的變量/活躍對象,函數的[[Scope]]屬性以及上下文的做用域鏈:
全局上下文的變量對象以下所示:
globalContext.VO === Global = {
x: 10
foo:
};
複製代碼
在「foo」函數建立的時候,其[[Scope]]屬性以下所示:
foo.[[Scope]] = [
globalContext.VO
];
複製代碼
在「foo」函數激活的時候(進入上下文時),「foo」函數上下文的活躍對象以下所示:
fooContext.AO = {
y: 20,
bar:
};
複製代碼
同時,「foo」函數上下文的做用域鏈以下所示:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
複製代碼
在內部「bar」函數建立的時候,其[[Scope]]屬性以下所示:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
複製代碼
在「bar」函數激活的時候,其對應的活躍對象以下所示:
barContext.AO = {
z: 30
};
複製代碼
同時,「bar」函數上下文的做用域鏈以下所示:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
複製代碼
以下是「x」,「y」和「z」標識符的查詢過程:
- "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
複製代碼
以下例子所示:
var x = 10;
function foo() {
alert(x);
}
(function () {
var x = 20;
foo(); // 10, but not 20
})();
複製代碼
咱們看到變量「x」是在「foo」函數的[[Scope]]中找到的。對於變量查詢而言,詞法鏈是在函數建立的時候就定義的,而不是在使用的調用的動態鏈(這個時候,變量「x」纔會是20)。
下面是另一個(典型的)閉包的例子:
function foo() {
var x = 10;
var y = 20;
return function () {
alert([x, y]);
};
}
var x = 30;
var bar = foo(); // anonymous function is returned
bar(); // [10, 20]
複製代碼
上述例子再一次證實了處理標識符的時候,詞法做用域鏈是在函數建立的時候定義的——變量「x」的值是10,而不是30。 而且,上述例子清楚的展現了函數(上述例子中指的是函數「foo」返回的匿名函數)的[[Scope]]屬性,即便在建立該函數的上下文結束的時候依然存在。
更多關於ECMAScript對閉包的實現細節會在第六章-閉包中作介紹。
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();
複製代碼
上述例子中,函數「barFn」就是經過Function構造器來建立的,這個時候變量「y」就沒法訪問到了。 但這並不意味着函數「barFn」就沒有內部的[[Scope]]屬性了(不然它連變量「x」都沒法訪問到了)。 問題就在於當函數經過Function構造器來建立的時候,其[[Scope]]屬性永遠都只包含全局對象。 哪怕在上層上下文中(非全局上下文)建立一個閉包都是無濟於事的。
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」函數的活躍對象有原型的話,屬性「x」則應當在Object.prototype中找到,由於它在AO中根本不存在。 然而,上述第一個例子中,在標識符處理階段遍歷了整個做用域鏈,到了全局對象(部分實現是這樣的),該對象繼承自Object.prototype,所以,最終變量「x」的值就變成了10。
一樣的狀況,在某些版本的SpiderMonkey中,經過命名函數表達式(簡稱:NFE)也會發生,其中的存儲了可選的函數表達式的名字的特殊對象也繼承自Object.prototype, 一樣的,在某些版本的Blackberry中,也是如此,其活躍對象是繼承自Object.prototype的。不過,關於這塊詳細的特性將會在第五章-函數中做介紹。
globalContext.Scope = [
Global
];
evalContext.Scope === callingContext.Scope;
複製代碼
Scope = withObject|catchObject + AO|VO + [[Scope]]
複製代碼
以下例子中,with語句添加了foo對象,使得它的屬性能夠不須要前綴直接訪問。
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
複製代碼
對應的做用域鏈修改成以下所示:
Scope = foo + AO|VO + [[Scope]]
複製代碼
接着來看下面這個例子:
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
複製代碼
發生了什麼?怎麼最外層的「y」變成了30? 在進入上下文的時候,「x」和「y」標識符已經添加到了變量對象。以後,到了執行代碼階段,發生了以下的改動:
一樣的,catch從句(能夠訪問參數異常)會建立一個只包含一個屬性(異常參數名)的新對象。以下所示:
try {
...
} catch (ex) {
alert(ex);
}
複製代碼
做用域鏈修改成以下所示:
var catchObject = {
ex:
};
Scope = catchObject + AO|VO + [[Scope]]
複製代碼
在catch從句結束後,做用域鏈一樣也會恢復到原先的狀態。
本文,介紹了幾乎全部與執行上下文相關的概念以及相應的細節。後面的章節中,會給你們介紹函數對象的細節:函數的類型(FunctionDeclaration,FunctionExpression)和閉包。 順便提下,本文中介紹過,閉包是和[[Scope]]有直接的關係,可是關於閉包的細節會在後續章節中做介紹。
重學JavaScript深刻理解系列(一)
重學JavaScript深刻理解系列(二)
重學JavaScript深刻理解系列(三)
重學JavaScript深刻理解系列(五)
重學JavaScript深刻理解系列(六)