一文讀懂JS運行機制(略長)

一、前言

var a = 'heihei', b = 'xixi'
function foo () {
  console.log(a)
}

function bar () {
  var a = 'houhou'
  foo()
  console.log(b)
}

bar()
// heihei
// xixi

如果您很快就能得出上述結果,那相信您的功底非常之紮實,如果無法確定,那麼這篇文章通讀之後,相信可以幫您解疑。

二、執行上下文

JS中可執行代碼有三種:全局代碼函數代碼eval代碼。代碼執行前需要準備的執行環境也稱爲執行上下文,所以也分爲全局執行上下文函數執行上下文eval執行上下文

2.1 全局執行上下文

全局執行上下文中的變量對象就是全局對象,預置了很多屬性和函數。在瀏覽器中是window,在NodeJS中是global

2.2 函數執行上下文

2.2.1 執行上下文初始化

  1. 複製函數[[scope]]屬性創建作用域鏈(下面會講到)
  2. arguments創建活動對象
  3. 初始化活動對象,即加入形參、函數聲明、變量聲明
  4. 將活動對象壓入作用域鏈頂端

2.2.2 變量聲明,聲明提升

變量對象被激活爲激活對象,此時發生"hoist"聲明提升

  1. 函數的所有形參

    • 鍵爲arguments,其值也是一個對象(類數組對象,有length屬性),按形參順序賦值,值爲實參
    • 若無實參,屬性值設爲 undefined
  2. 函數聲明

    • 在變量對象中添加以函數名命名的屬性,它的值是一個指向堆區(堆內存)函數Function對象的引用。
    • 如果這個函數名字在變量對象屬性中已存在,這個引用指針就會被重寫,指向堆區中當前的函數對象。
  3. 變量聲明

    • 由名稱和對應值(undefined)組成一個變量對象的屬性被創建;
    • 如果變量名稱跟已經聲明的形參或函數相同,則變量聲明不會干擾已經存在的這類屬性(即形參和函數聲明的優先級高於變量聲明提升)

注意: 整個過程可以大概描述成: 函數的形參=>函數聲明=>變量聲明, 其中在創建函數聲明時,如果名字存在,則會被重寫,在創建變量時,如果變量名存在,則忽略不會進行任何操作。

2.2.3 當代碼執行時

根據代碼修改激活對象對象中對應的值。如果當前執行上下文中的變量對象沒有該屬性,就去父級的執行上下文變量對象中尋找,直至到全局執行上下文。找不到就報錯

2.3 eval執行上下文

eval執行上下文比較特殊,它取決於eval函數是直接調用還是間接調用。
引用MDN上的說法:

如果間接的使用eval(),比如通過一個引用來調用它,而不是直接的調用eval。 從 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。

  • 當直接調用時,eval執行上下文爲執行時所處的執行上下文,具有和這個執行上文相同的作用域
  • 當間接調用時,eval執行上下文爲全局執行上下文
function foo () {
  var x = 2, y = 4
  console.log(eval('x + y'))  // 直接調用,執行上下文爲當前函數執行上下文,結果是 6

  var geval = eval // 等價於在全局作用域調用
  console.log(geval('x + y')) // 間接調用,執行上下文爲全局執行上下文,x is not defined,實際上y也是not defined
  
  console.log(window.eval('x + y')) // 這也是間接調用
}
foo()

三、執行上下文棧

JS通過執行上下文棧來管理上述這些執行上下文

  1. JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以首先就會創建一個全局執行上下文,並壓入執行上下文棧,我們用globalContext表示它,並且只有當整個應用程序結束的時候,ECStack纔會被清空,所以程序結束之前,ECStack最底部永遠有個globalContext
  2. 執行函數時,就會生成一個函數執行上下文並推入執行上下文棧,當函數執行完成就會把這個函數執行上下文彈出,並將控制權移交至執行棧中下一個執行環境,直至全局執行上下文globalContext
  3. 當程序結束或者瀏覽器關閉,全局執行上下文也會從執行棧中彈出並銷燬
function foo (a) {
  console.log(a)
}
function bar (b) {
  foo(b)
}

bar('hehe')

在這裏插入圖片描述

四、變量對象(variable object)與激活對象(activation object)

  • 變量對象(variable object, VO):每個執行上下文都一個與之對應的變量對象,它是與執行上下文相關的數據作用域,存儲了在上下文中的函數標識符、形參、變量聲明等。但在規範上或者引擎實現上,這個對象是不能在JS環境中訪問的
  • 激活對象(activation object, AO):當進入某個函數執行上下文中時,其對應變量對象會被激活,變量對象上的屬性才能被訪問,所以稱之爲激活對象。

激活對象就是在函數執行上下文中被激活成可訪問的變量對象

五、詞法環境(lexical environment)

根據詞法環境規範定義:

  • A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
  • 詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由環境記錄(environment record)和可能爲空引用(null)的外部詞法環境組成。

環境記錄:主要是聲明性環境記錄(declarative Environment Records)和對象環境記錄(object Environment Records),其次還有全局環境記錄(global Environment Records)和函數環境記錄(function Environment Records)。

  • 聲明性環境記錄(declarative Environment Records):存儲變量、函數和參數, 用於函數聲明、變量聲明和catch語句。

  • 對象環境記錄(object Environment Records):用於像with這樣綁定對象標識符(作用域)的語句。

  • 全局環境記錄函數環境記錄:是特殊的聲明性環境記錄,形式上可理解爲對應的變量對象。

外部詞法環境:構成作用域鏈的關鍵

六、作用域與作用域鏈

6.1 作用域

  • 詞法作用域(靜態作用域):函數的作用域在函數定義的時候就決定了。JS使用的是詞法作用域。
  • 動態作用域:函數的作用域是在函數調用的時候才決定的。

作用域(Scope)用於規定如何查找變量,也就是確定當前執行上下文中對變量的訪問權限。

6.2 作用域鏈

在函數中有一個內部屬性,當函數創建的時候,就會保存所有父變量對象到其中,在查找變量值的時候,會先從[[scope]]頂部即當前上下文的變量對象(作用域)中查找,如果沒有找到,就會根據當前執行上下文中的[[scope]]對外部執行環境的引用順序,從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈

注意

  1. 當進入函數執行上下文(函數激活)時,會將該函數的變量對象推入到作用域鏈前端。
  2. 正式由於作用域與作用域鏈的這種關係,在當前函數執行上下文的活動對象中必定存在this和arguments,所以this和arguments的搜索在當前執行執行上下文就停止了。

總結

回過頭來看前言中的問題,按照下面的流程進行(只列出關鍵部分):

  1. 創建全局執行上下文,推入執行上下文棧中
ECStack = [
  globalContext
]
  1. 初始化全局執行上下文
globalContext = {
  VO: [global], // 指向全局對象
  Scope: [globalContext.VO], // 可訪問權限
  this: globalContext.VO
}
  1. 同時foo函數和bar函數被創建,生成內部作用域鏈。
foo.[[scope]] = [
  globalContext.VO
]

bar.[[scope]] = [
  globalContext.VO
]
  1. 經過代碼執行,全局執行環境的變量對象已經賦值。根據2.2節中所述,執行bar函數前,創建bar函數執行上下文,並推入執行上下文棧中。
ECStack = [
  barContext,
  globalContext
]
  1. 初始化bar函數執行上下文
barContext = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined,
    foo: <reference to function foo() {}>
  },
  Scope: [barContext.AO, globalContext.VO],
  this: undefined
}
  1. 中斷bar函數執行,開始執行foo函數,同理,創建foo函數執行上下文,並推入執行上下文棧中
ECStack = [
  fooContext,
  barContext,
  globalContext
]
  1. 初始化foo函數執行上下文
fooContext = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [fooContext.AO, globalContext.VO],
  this: undefined
}
  1. foo函數執行,foo函數執行上下文中的激活對象沒有屬性a,所以沿着作用域鏈[[scope]]找到全局執行上下文中的變量對象,其指向全局對象,故輸出'heihei'。執行完畢彈出foo函數執行上下文並銷燬。
ECStack = [
  barContext,
  globalContext
]
  1. 繼續bar函數執行,bar函數執行上下文中的激活對象沒有屬性b,所以沿着作用域鏈[[scope]]找到全局執行上下文中的變量對象,其指向全局對象,故輸出'xixi'。執行完畢彈出bar函數執行上下文並銷燬。
ECStack = [
  globalContext
]

參考

  1. 規範文檔
  2. MDN
  3. 傻傻分不清的javascript運行機制
  4. javascript作用域,作用域鏈,[[scope]]屬性
  5. JavaScript深入之作用域鏈
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章