傻傻分不清的javascript運行機制

一, 執行環境和執行棧

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

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

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

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

執行環境的分類:es6

  • 全局執行環境:簡單的理解,一個程序只有一個全局對象即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 規範學習

建立階段:this

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

激活/執行階段:spa

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

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語言最重要的一步。

歡迎工做一到五年的Java工程師朋友們加入Java架構開發:855801563 獲取更多免費視頻教程。

合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代

相關文章
相關標籤/搜索