傻傻分不清的javascript運行機制


學習到javascript的運行機制時,有幾個概念經常出現在各種文章中且容易混淆。Execution Context(執行環境或執行上下文),Context Stack (執行棧),Variable Object(VO: 變量對象),Active Object(AO: 活動對象),LexicalEnvironment(詞法環境),VariableEnvironment(變量環境)等,特別是 VO,AO以及LexicalEnvironment,VariableEnvironment的區別很多文章都沒有涉及到。因此我查看了一些國內外的文章,結合自身理解寫下了下面的筆記。雖然因為自身不足導致理解上的偏差,但是依然相信讀完下文會對理解javascript的一些概念如變量提升,作用域和閉包有很大的幫助。

一, 執行環境和執行棧

了解javascript的運行機制,首先必須掌握兩個基本的概念。Execution Context(執行環境或執行上下文)和Context Stack (執行棧)

1. 何為執行環境(執行上下文)(Execution Context)

我們知道javascript是單線程語言,也就是同一時間只能執行一個任務。當javascript解釋器初始化代碼后,默認會進入全局的執行環境,之后每調用一個函數,javascript解釋器會創建一個新的執行環境。

    var a = 1;                       // 1.初始化默認進入全局執行環境
    function b() {                   // 3.進入b 的執行環境
        function c() {               // 5. 進入c的執行環境
            ···
        }
        c()                          // 4.在b的執行環境里調用c, 創建c的執行環境
    }
    b()                              // 2. 調用b 創建 b 的執行環境

執行環境的分類:

  • 全局執行環境:簡單的理解,一個程序只有一個全局對象即window對象,全局對象所處的執行環境就是全局執行環境。
  • 函數執行環境:函數調用過程會創建函數的執行環境,因此每個程序可以有無數個函數執行環境。
  • Eval執行環境:eval代碼特定的環境。

2. 如何單線程運行(Context Stack)

從一個簡單的例子開始講起

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

如何存儲代碼運行時的執行環境(全局執行環境,函數執行環境)呢,答案是執行棧。而棧遵循的是先進后出的原理,javascript初始化完代碼后,首先會創建全局執行環境並推入當前的執行棧,當調用一個函數時,javascript引擎會創建新的執行環境並推到當前執行棧的頂端,在新的執行環境中,如果繼續發生一個新函數調用時,則繼續創建新的執行環境並推到當前執行棧的頂端,直到再無新函數調用。最上方的函數執行完成后,它的執行環境便從當前棧中彈出,並將控制權移交到當前執行棧的下一個執行環境,直到全局執行環境。當程序或瀏覽器關閉時,全局環境也將退出並銷毀。

因此輸出的結果為:

begin:2
begin:1
begin:0
end:0
end:1
end:2

3. 如何創建執行環境

我們現在知道每次調用函數時,javascript 引擎都會創建一個新的執行環境,而如何創建這一系列的執行環境呢,答案是執行器會分為兩個階段來完成, 分別是創建階段和激活(執行)階段。而即使步驟相同但是由於規范的不同,每個階段執行的過程有很大的不同。

3.1 ES3 規范

創建階段:

  • 1.創建作用域鏈。
  • 2.創建變量對象VO(包括參數,函數,變量)。
  • 3.確定this的值。

激活/執行階段:

  • 完成變量分配,執行代碼。
3.2 ES5 規范

創建階段:

  • 1.確定 this 的值。
  • 2.創建詞法環境(LexicalEnvironment)。
  • 3.創建變量環境(VariableEnvironment)。

激活/執行階段:

  • 完成變量分配,執行代碼。

我們從規范上可以知道,ES3和ES5在執行環境的創建階段存在差異,當然他們都會在這個階段確定this 的值
(關於this 的指向問題我們以后會在專門的文章中分析各種this 的指向問題,這里便不做深究)。我們將圍繞這兩個規范不同點展開。盡管ES3的一些規范已經被拋棄,但是掌握ES3 創建執行環境的過程依然有助於我們理解javascript深層次的概念。

二, Variable Object(VO: 變量對象),Active Object(AO: 活動對象)

2.1 基本概念

VO 和 AO 是ES3規范中的概念,我們知道在創建過程的第二個階段會創建變量對象,也就是VO,它是用來存放執行環境中可被訪問但是不能被 delete 的函數標識符,形參,變量聲明等,這個對象在js環境下是不可訪問的。而AO 和VO之間區別就是AO 是一個激活的VO,僅此而已。

  • 變量對象(Variable) object)是說JS的執行上下文中都有個對象用來存放執行上下文中可被訪問但是不能被delete的函數標示符、形參、變量聲明等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規范上或者說是引擎實現上的不可在JS環境中訪問到活動對象

  • 激活對象(Activation object)有了變量對象存每個上下文中的東西,但是它什么時候能被訪問到呢?就是每進入一個執行上下文時,這個執行上下文兒中的變量對象就被激活,也就是該上下文中的函數標示符、形參、變量聲明等就可以被訪問到了

2.2 執行細節

如何創建VO對象可以大致分為四步

  • 1.創建arguments對象
  • 2.掃描上下文的函數聲明(而非函數表達式),為發現的每一個函數,在變量對象上創建一個屬性——確切的說是函數的名字——其有一個指向函數在內存中的引用。如果函數的名字已經存在,引用指針將被重寫。
  • 3.掃描上下文的變量聲明,為發現的每個變量聲明,在變量對象上創建一個屬性——就是變量的名字,並且將變量的值初始化為undefined。如果變量的名字已經在變量對象里存在,將不會進行任何操作並繼續掃描。

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

一個簡單的例子

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

執行的偽代碼

// 創建階段
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}
// 激活階段
fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

三, LexicalEnvironment(詞法環境),VariableEnvironment(變量環境)

3.1 基本概念

詞法環境和變量環境是ES5以后提到的概念,官方對詞法環境的解釋如下。

詞法環境是一種規范類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關系。詞法環境由環境記錄(environment record)和可能為空引用(null)的外部詞法環境組成。

簡單的理解,詞法環境是一個包含標識符變量映射的結構。(這里的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用)。

ES3的VO,AO為什么可以被拋棄?個人認為有兩個原因,第一個是在創建過程中所執行的創建作用域鏈和創建變量對象(VO)都可以在創建詞法環境的過程中完成。第二個是針對es6中存儲函數聲明和變量(let 和 const)以及存儲變量(var)的綁定,可以通過兩個不同的過程(詞法環境,變量環境)區分開來。

3.2 詞法環境(lexicalEnvironment)

詞法環境由兩個部分組成

  • 環境記錄(enviroment record),存儲變量和函數聲明
  • 對外部環境的引用(outer),可以通過它訪問外部詞法環境

對外部環境的引用關系到作用域鏈,之后再分析,我們先來看看環境記錄的分類。

環境記錄分兩部分

  • 聲明性環境記錄(declarative environment records): 存儲變量、函數和參數, 但是主要用於函數 、catch詞法環境。
    注意:函數環境下會存儲arguments的值。而詳細的過程可以參考VO 的執行細節,基本大同小異
  • 對象環境記錄(object environment records), 主要用於with 和全局的詞法環境

偽代碼如下

// 全局環境
GlobalExectionContext = {  
// 詞法環境
  LexicalEnvironment: {  
    EnvironmentRecord: {  
    
        ···
    }
    outer: <null>  
  }  
}
// 函數環境
FunctionExectionContext = {  
// 詞法環境
  LexicalEnvironment: {  
    EnvironmentRecord: {  
        // 包含argument
        
    }
    outer: <Global or outer function environment reference>  
  }  
}

3.3 變量環境(objectEnvironment)

變量環境也是個詞法環境,主要的區別在於lexicalEnviroment用於存儲函數聲明和變量( let 和 const )綁定,而ObjectEnviroment僅用於存儲變量( var )綁定。

3.4 偽代碼展示

ES5規范下的整個創建過程可以參考下方的偽代碼

let a = 20;  
const b = 30;  
var c;

function d(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = d(20, 30);
// 全局環境
GlobalExectionContext = {

  this: <Global Object>,
    // 詞法環境
  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  // 環境記錄分類: 對象環境記錄
      a: < uninitialized >,  // 未初始化
      b: < uninitialized >,  
      d: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  // 環境記錄分類: 對象環境記錄
      c: undefined,  // undefined
    }  
    outer: <null>  
  }  
}
// 函數環境
FunctionExectionContext = {  
   
  this: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  // 環境記錄分類: 聲明環境記錄
      Arguments: {0: 20, 1: 30, length: 2},  // 函數環境下,環境記錄比全局環境下的環境記錄多了argument對象
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  // 環境記錄分類: 聲明環境記錄
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

四,作用域鏈

前面講創建過程中,我們留下了一個伏筆,ES3規范中有創建作用域鏈的過程,而ES5中在創建詞法環境或變量環境的過程中,也有生成外部環境的引用的過程。那這個過程有什么作用呢。我們通過一個簡單的例子來說明。

function one() {

    var a = 1;
    two();

    function two() {

        var b = 2;
        three();

        function three() {

            var c = 3;
            alert(a + b + c); // 6

        }

    }

}

one();

當執行到three 的執行環境時,此時 a和b 都不在c 的變量內,因此作用域鏈則起到了引用外部執行環境變量的作用。ES3中創建的作用域鏈如圖:

當解釋器執行alert(a + b + c),他首先會找自身執行環境下是否有a這個變量的存在,如果不存在,則通過查看作用域鏈,判斷a是否在上一個執行環境內部。它檢查是否a存在於內部,若找不到,則沿着作用域鏈往上一個執行環境找,直到找到,或者到頂級的全局作用域。同理ES6規范中也可以這樣分析。

因此這會引入一個javascript一個重要的概念,閉包。從上面對執行環境的解釋我們可以這樣理解,閉包就是內部環境通過作用域鏈訪問到上層環境的變量。因此也存在無法進行變量回收的問題,只要函數的作用域鏈在,變量的值便因為閉包無法被回收。

注意: 此作用域鏈和原型鏈的作用域鏈不是同一個概念。

五, 小結

通過對javascript運行機制的介紹,對一些javasript高級概念有了更深的認識,特別是對一些雲里霧里的概念區別有了更深刻的認識。不同規范下,不同概念的解釋更有利於深挖javascript底層的執行思想。我相信這是理解javascipt語言最重要的一步。

參考資料:

本文為博主原創文章,轉載請注明出處 https://juejin.im/post/5c20526b6fb9a049b7805ff9


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM