開篇
作用域是每種計算機語言最重要的基礎之一,因此要想深入的學習JavaScript,作用域和作用域鏈就是個繞不開的話題。
在《深入學習js之—-執行上下文棧》中我們提到過,當JavaScript代碼執行一段可執行代碼(executable code)時,會創建對應的執行上下文(execution context)。
對於每個執行上下文,都有三個重要屬性:
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
今天重點聊聊作用域鏈。
作用域
細說作用域鏈之前,我們首先來聊聊作用域,簡單的說,作用域就是變量與函數的可訪問範圍,即作用域控制着變量與函數的可見性和生命週期。
在JavaScript中,變量的作用域有全局作用域和局部作用域兩種(局部作用域又稱爲函數作用域)。
作用域鏈
代碼在當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。
這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈。
下面,讓我們以一個函數的創建和激活兩個時期來講解作用域鏈是如何創建和變化的。
函數創建
在《深入學習js之——詞法作用域和動態作用域》中講到,函數的作用域在函數定義的時候就決定了——即JavaScript採用的是靜態作用域。
這是因爲函數有一個內部屬性 [[scope]]
,當函數創建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]]
就是所有父變量對象的層級鏈,但是注意:[[scope]]
並不代表完整的作用域鏈!
舉個例子:
function foo(){
function bar(){
...
}
}
函數創建時,各自的[[scope]]
爲:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
函數激活
當函數激活時,進入函數上下文,創建 VO/AO 後,就會將活動對象添加到作用鏈的前端。
這時候執行上下文的作用域鏈,我們命名爲 Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域鏈創建完畢。
通過例子深刻理解
以下面的例子爲例,結合着之前講的變量對象和執行上下文棧,我們來總結一下函數執行上下文中作用域鏈和變量對象的創建過程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
執行過程如下:
1.checkscope 函數被創建,保存作用域鏈到 內部屬性[[scope]]
;
checkscope.[[scope]] = [
globalContext.VO
];
2.執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧;
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函數並不立刻執行,開始做準備工作,第一步:複製函數[[scope]]
屬性創建作用域鏈;
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 創建活動對象,隨後初始化活動對象,加入形參、函數聲明、變量聲明;
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:將活動對象壓入 checkscope 作用域鏈頂端;
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.準備工作做完,開始執行函數,隨着函數的執行,修改 AO 的屬性值;
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回後函數執行完畢,函數上下文從執行上下文棧中彈出;
ECStack = [
globalContext
];
參考:
歡迎添加我的個人微信討論技術和個體成長。
歡迎關注我的個人微信公衆號——指尖的宇宙,更多優質思考乾貨