你須要知道的JS運行機制

1、前言

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

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

bar()
// heihei
// xixi
複製代碼

若是您很快就能得出上述結果,那相信您的功底很是之紮實,若是沒法肯定,那麼這篇文章通讀以後,相信能夠幫您解疑。javascript

2、執行上下文

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

2.1 全局執行上下文

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

2.2 函數執行上下文

2.2.1 執行上下文初始化

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

2.2.2 變量聲明,聲明提高

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

  1. 函數的全部形參git

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

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

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

注意: 整個過程能夠大概描述成: 函數的形參=>函數聲明=>變量聲明, 其中在建立函數聲明時,若是名字存在,則會被重寫,在建立變量時,若是變量名存在,則忽略不會進行任何操做。數組

2.2.3 當代碼執行時

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

2.3 eval執行上下文

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

若是間接的使用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()
複製代碼

3、執行上下文棧

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

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

bar('hehe')
複製代碼

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

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

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

5、詞法環境(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、做用域與做用域鏈

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深刻之做用域鏈
相關文章
相關標籤/搜索