看完冴羽大大寫的作用域鏈分析的文章腦子還是有點迷糊,在此再針對其中的 “函數執行上下文中作用域鏈和變量對象的創建過程” 幫助自己重新做一份梳理和學習記錄。
1. 執行上下文
當JS代碼執行到可執行函數時,即運行到執行函數的地方,會創建其對應的執行上下文。
每個執行上下文都有三個非常重要的屬性,即:
- 變量對象(VO) (函數執行時的活動對象: AO)
- 作用域鏈(Scope Chain)
- this
2. 什麼是作用域鏈?
JS在查找變量時,會先從當前執行上下文的變量對象中查找,如果沒找到,則從父級執行上下文的變量對象中查找,依次向上遞推,一直找到全局上下文的變量對象爲止。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈
3. JS中作用域鏈的創建和變化過程
JS中作用域鏈的創建和變化分爲兩個時期: 函數創建時期 和 函數激活時期
-
3.1 函數創建時期
由於JS採用的是靜態作用域(詞法作用域),所以函數的作用域在函數定義時就已經決定了,而與函數調用位置無關。
導致上述現象的原因與JS函數的一個內置屬性 [[scope]] 有關。每一個函數創建後(定義後),其內部的[[scope]] 都會將該函數所有的父變量對象(VO)保存到其中。形成父變量對象的一個層級鏈。但是,這並不是完整的作用域鏈 !function foo(){ function bar(){ } }
這兩個函數創建時,其內部各自的[[scope]]是這樣的,[[scope]]中保存各自所有的父變量對象: foo.[[scope]] = [ globalContext.VO ] bar.[[scope]] = [ fooContext.AO, globalContext.VO ]
-
3.2 函數激活時期
函數激活,即開始執行函數的時候,會進入函數上下文,創建該函數的變量對象(VO/AO)之後,將該變量對象添加到作用域鏈的前端。形成最終完整的作用域鏈。
這時執行上下文完成的作用域鏈我們稱爲Scope:Scope = [AO].concat([[Scope]]) // 數組拼接
4.通過一個小Demo分析完整作用域鏈的形成過程。
var scope = "globalScope";
function checkScope(){
var scope2 = "localScope";
return scope2;
}
checkScope();
分析執行過程:
-
checkScope函數被定義的時候即 創建checkScope函數,
checkScope內置的[[scope]]屬性會保存其所有父級VO,這裏就是全局執行上下文的VOcheckScope.[[scope]] = [ globalContext.VO ]
-
當調用checkScope函數的時候,會形成其對應的執行上下文,checkScope函數被壓入執行上下文棧(如果checkScope函數裏面還調用了其他函數,那些函數也會被依次壓入執行上下文棧):
ECStack = [ (push) checkScopeContext, globalContext ]
-
此時checkScope函數不會立即從棧中pop出來執行。 而是會先進行一些**“準備工作”**:
第一步: 複製checkScope函數創建時的父級VO作用域鏈,作爲對象保存到其執行上下文對象中:
// check checkScopeContext = { Scope: checkScope.[[scope]] }
第二步: 生成checkScope自己的變量對象AO: 用arguments創建活動對象,隨後初始化活動對象,加入形參、函數聲明以及變量聲明:
AO: { arguments: [ length: 0 ], scope: undefined } // 這是新創建的AO
第三步: 將新創建的AO壓入原來的作用域頂端形成完整的作用域鏈:
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, // checkScope的活動變量 Scope: [AO, [[Scope]]] // 完整的作用域鏈 }
-
準備工作完成後,開始執行函數。首先根絕代碼執行順序依次修改AO中的屬性值。
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] }
-
執行函數時將checkScopeContext從上下文執行棧ECStack中彈出。函數執行完畢。
ECStack = [ (pop) checkScopeContext, globalContext ]