深刻JavaScript系列(二):執行上下文

1、執行上下文(Exexution Contexts)

執行上下文(Exexution Contexts):用來經過ECMAScript編譯器來追蹤代碼運行時計算的一種規範策略。git

執行上下文簡單理解就是代碼執行時所在環境的抽象。github

執行上下文同時包含變量環境組件(VariableEnvironment)詞法環境組件(LexicalEnvironment),這兩個組件多數狀況下都指向相同的詞法環境(Lexical Environment),那爲何還要存在兩個環境組件呢?咱們稍後將進行詳細討論。若是不太瞭解詞法環境的能夠看下個人上一篇文章深刻ECMAScript系列(一):詞法環境函數

ExecutionContext = {
    VariableEnvironment: { ... },
    LexicalEnvironment: { ... },
}
複製代碼

2、執行上下文棧

執行上下文棧(Execution Context Stack):是一個後進先出的棧式結構(LIFO),用來跟蹤維護執行上下文。運行執行上下文(running execution context) 始終位於執行上下文棧的頂層。那麼何時會建立新的執行上下文呢?post

ECMAScript可執行代碼有四種類型:全局代碼,函數代碼,模塊代碼和eval。每當從當前執行代碼運行至其餘可執行代碼時,會建立新的執行上下文,將其壓入執行上下文棧併成爲正在運行的執行上下文。當相關代碼執行完畢返回後,將正在運行的執行上下文從執行上下文棧刪除,以前的執行上下文又成爲了正在運行的執行上下文。ui

咱們經過一個動圖來看一下執行上下文棧的工做過程spa

  1. 開始執行任何JavaScript代碼前,會建立全局上下文並壓入棧,因此全局上下文一直在棧底。
  2. 每次調用函數都會建立新的執行上下文(即使在函數內部調用自身),並壓入棧。
  3. 函數執行完畢返回,其執行上下文出棧。
  4. 全部代碼運行完畢,執行上下文棧只剩全局執行上下文。

3、執行上下文的建立、入棧及出棧

上面提到過ECMAScript可執行代碼有四種類型:全局代碼,函數代碼,模塊代碼和evalcode

這裏雖說是全局代碼,可是JavaScript引擎實際上是按照script標籤來解析執行的,也就是說script標籤按照它們出現的順序解析執行,這也就是爲何咱們平時要將項目依賴js庫放在前面引入的緣由。cdn

JavaScript引擎是按可執行代碼塊來執行代碼的,在任意的JavaScript可執行代碼被執行時,執行步驟可按以下理解:對象

  1. 建立一個新的執行上下文(Execution Context)
  2. 建立一個新的詞法環境(Lexical Environment)
  3. 將該執行上下文的 變量環境組件(VariableEnvironment)詞法環境組件(LexicalEnvironment) 都指向新建立的詞法環境
  4. 將該執行上下文 推入執行上下文棧 併成爲 正在運行的執行上下文
  5. 對代碼塊內的 標識符進行實例化及初始化
  6. 運行代碼
  7. 運行完畢後執行上下文出棧

變量提高(Hoisting)及暫時性死區(temporal dead zone,TDZ)

咱們日常所說的變量提高就發生在上述執行步驟的第四步,對代碼塊內的標識符進行實例化及初始化的具體表現以下:ip

  1. 執行代碼塊內的letconstclass聲明的標識符合集記錄爲lexNames
  2. 執行代碼塊內的varfunction聲明的標識符合集記錄爲varNames
  3. 若是lexNames內的任何標識符在varNameslexNames內出現過,則報錯SyntaxError

    這就是爲何能夠用varfunction聲明多個同名變量,可是不能用letconstclass聲明多個同名變量。

  4. varNames內的var聲明的標識符實例化並初始化賦值undefined,若是有同名標識符則跳過

    這就是所謂的變量提高,咱們用var聲明的變量,在聲明位置以前訪問並不會報錯,而是返回undefined

  5. lexNames內的標識符實例化,但並不會進行初始化,在運行至其聲明處代碼時纔會進行初始化,在初始化前訪問都會報錯。

    這就是咱們所說的暫時性死區letconstclass聲明的變量其實也提高了,只不過沒有被初始化,初始化以前不可訪問。

  6. 最後將varNames內的函數聲明實例化並初始化賦值對應的函數體,若是有同名函數聲明,則前面的都會忽略,只有最後一個聲明的函數會被初始化賦值。

    函數聲明會被直接賦值,全部咱們在函數聲明位置以前也能夠調用函數。

4、爲何須要兩個環境組件

首先明確這兩個環境組件的做用,變量環境組件(VariableEnvironment)用於記錄var聲明的綁定,詞法環境組件(LexicalEnvironment)用於記錄其餘聲明的綁定(如letconstclass等)。

通常狀況下一個Exexution Contexts內的VariableEnvironmentLexicalEnvironment指向同一個詞法環境,之因此要區分兩個組件,主要是爲了實現塊級做用域的同時不影響var聲明及函數聲明

衆所周知,ES6以前並無塊級做用域的概念,可是ES6及以後咱們能夠經過新增的letconst等命令來實現塊級做用域,而且不影響var聲明的變量和函數聲明,那麼這是怎麼實現的呢?

  1. 首先在一個正在運行的執行上下文(running Execution Context)內,詞法環境由VariableEnvironmentLexicalEnvironment構成,此執行上下文內的全部標識符的綁定都記錄在兩個組件的環境記錄內。
  2. 當運行至塊級代碼時,會將LexicalEnvironment記錄下來,咱們將其記錄爲oldEnv
  3. 而後建立一個新的LexicalEnvironment(外部詞法環境outer指向oldEnv),咱們將其記錄爲newEnv,並將newEnv設置爲running Execution ContextLexicalEnvironment
  4. 而後塊級代碼內的letconst等聲明就會綁定在這個newEnv上面,可是var聲明和函數聲明仍是綁定在原來的VariableEnvironment上面。

    塊級代碼內的函數聲明會被當作var聲明,會被提高至外部環境,塊級代碼運行前其值爲初始值undefined

    console.log(foo) // 輸出:undefined
    {
        function foo() {console.log('hello')}
    }
    console.log(foo) // 輸出: ƒ foo() {console.log('hello')}
    複製代碼
  5. 塊級代碼運行完畢後,又將oldEnv還原爲running Execution ContextLexicalEnvironment

目前包括塊級代碼(在一對大括號內的代碼)、for循環語句、switch語句、TryCatch語句中的catch從句以及with語句(with語句建立的新環境爲對象式環境,其餘皆爲聲明式環境)都是這樣來實現塊級做用域的。

系列文章

準備將以前寫的部分深刻ECMAScript文章重寫,加深本身理解,使內容更有乾貨,目錄結構也更合理。

深刻ECMAScript系列目錄地址(持續更新中...)

歡迎前往閱讀系列文章,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

菜鳥一枚,若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤,與你們共同進步。

相關文章
相關標籤/搜索