前言
在《JavaScript深入之執行上下文棧》中講到,當JavaScript程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。
對於每個執行上下文,都有三個重要屬性:
- 變數物件(Variable object,VO)
- 作用域鏈(Scope chain)
- this
今天重點講講作用域鏈。
作用域鏈
在《JavaScript深入之變數物件》中講到,當查詢變數的時候,會先從當前上下文的變數物件中查詢,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變數物件中查詢,一直找到全域性上下文的變數物件,也就是全域性物件。這樣由多個執行上下文的變數物件構成的連結串列就叫做作用域鏈。
下面,讓我們以一個函式的建立和啟用兩個時期來講解作用域鏈是如何建立和變化的。
函式建立
在《JavaScript深入之詞法作用域和動態作用域》中講到,函式的作用域在函式定義的時候就決定了。
這是因為函式有一個內部屬性[[scope]],當函式建立的時候,就會儲存所有父變數物件到其中,你可以理解[[scope]]就是所有父變數物件的層級鏈。(注意:[[scope]]並不代表完整的作用域鏈!)
舉個例子:
1 2 3 4 5 |
function foo() { function bar() { ... } } |
函式建立時,各自的[[scope]]為:
1 2 3 4 5 6 7 8 |
foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.VO ]; |
函式啟用
當函式啟用時,進入函式上下文,建立VO/AO後,就會將活動物件新增到作用鏈的前端。
這時候執行上下文的作用域鏈,我們命名為Scope:
1 |
Scope = [AO].concat([[Scope]]); |
至此,作用域鏈建立完畢。
捋一捋
以下面的例子為例,結合著之前講的變數物件和執行上下文棧,我們來總結一下函式執行上下文中作用域鏈和變數物件的建立過程:
1 2 3 4 5 6 |
var scope = "global scope"; function checkscope(){ var scope2 = 'local scope'; return scope2; } checkscope(); |
執行過程如下:
1.checkscope函式被建立,儲存作用域鏈到[[scope]]
1 2 3 |
checkscope.[[scope]] = [ globalContext.VO ]; |
2.執行checkscope函式,建立checkscope函式執行上下文,checkscope函式執行上下文被壓入執行上下文棧
1 2 3 4 |
ECStack = [ checkscopeContext, globalContext ]; |
3.checkscope函式並不立刻執行,開始做準備工作,第一步:複製函式[[scope]]屬性建立作用域鏈
1 2 3 |
checkscopeContext = { Scope: checkscope.[[scope]], } |
4.第二步:用arguments建立活動物件,隨後初始化活動物件,加入形參、函式宣告、變數宣告
1 2 3 4 5 6 7 8 |
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined } } |
5.第三步:將活動物件壓入checkscope作用域鏈頂端
1 2 3 4 5 6 7 8 9 |
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } |
6.準備工作做完,開始執行函式,隨著函式的執行,修改AO的屬性值
深入系列
JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念,與羅列它們的用法不同,這個系列更注重通過寫demo,捋過程、模擬實現,結合ES規範等方法來講解。
所有文章和demo都可以在github上https://github.com/mqyqingfeng/Blog找到。如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。
本系列: