深入學習js之——作用域鏈#5

MagicalLouis發表於2019-02-23

開篇

作用域是每種計算機語言最重要的基礎之一,因此要想深入的學習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
];
複製程式碼

參考:

《JavaScript深入之作用域鏈》

《深入瞭解JavaScript,從作用域鏈開始》

深入學習JavaScript系列目錄

歡迎新增我的個人微信討論技術和個體成長。

深入學習js之——作用域鏈#5
歡迎關注我的個人微信公眾號——指尖的宇宙,更多優質思考乾貨

深入學習js之——作用域鏈#5

相關文章