深入理解js的執行機制

feaswcy發表於2019-05-06

寫在前面

javascript在瀏覽器中被瀏覽器的js引擎執行解釋,從執行上下文的角度分析一下js的執行機制

執行上下文

執行上下文被定義成javascript引擎在處理理解js程式碼時,所建立的一個動態的環境。理解執行上下文對理解javascript的執行機制至關重要。this指向問題、原型鏈、作用域和垃圾回收都與這個環境密切相關。

javascript程式碼在執行時的相關概念

EC (Execution Context),執行上下文 ECS (Execution Context Stack),執行環境棧,棧在計算機記憶體中是一種後入先出的資料結構 VO(Variable Object),變數物件。在js執行之前先確定,這一階段根據函式形參、函式宣告、變數宣告進行建立,其中在建立函式宣告時,如果名字存在,則會被重寫,在建立變數時,如果變數名存在,則忽略不會進行任何操作。 AO(Active Obejct),活動物件,實際執行時,就是程式碼在執行時變數的賦值和計算。可以理解成,AO = function(VO)

在執行js程式碼之前,js引擎會建立好當前程式碼的執行環境(EC),執行環境在js中可以分為三種:

  • 全域性程式碼,程式碼首次被執行的預設環境,可以理解成一個js 檔案或者js片段預設所在的環境
  • 函式程式碼,每次進入一個函式體,函式體執行之前
  • eval程式碼,eval是一個可以執行string型別的javascript方法

執行上下文與執行上下文堆疊

當瀏覽器首次載入指令碼,根據程式碼建立全域性執行環境,建立的要點可以抽象成確認三個要點:變數、作用域鏈、this指向,確認了這三點之後,開始進入啟用/執行程式碼階段,這時程式從上到下執行,遇到非同步程式碼會壓入任務佇列,如果在全域性程式碼中呼叫一個函式,程式引擎將會進入被呼叫的函式,並建立一個新的上下文,同時將建立的上下文壓入執行棧ECS頂部。執行棧是一個後入先出的結構,最後一個被壓入的函式將會最先執行。執行完成之後依次從棧中出棧,執行棧陸續被清空,最後回到全域性環境,一直到最後一行程式碼執行完畢,啟用/執行程式碼階段結束。為了解釋執行上下文棧的執行特點,我們看一下一面一組簡單的程式碼:

function foo(i) {  
    if(i === 3) { 
        return
    }  
    foo(i+1);  
    console.log(i);  
}  
foo(0);
複製程式碼

上面是一個自執行函式,我們來模仿一下javascript引擎在看到這段程式碼時處理的思路: 首先看到這段程式碼,預設建立的全域性執行環境可以抽象成一個object:

globalExecutionContextObj = {
    scopeChain: { },  /* 變數物件(variableObject)+ 所有父執行上下文的變數物件*/ 
    variableObject: {  
        foo: [function] //
    }, /*函式 arguments/引數,內部變數和函式宣告 */
    this: window  // 嚴格模式下為undefinednode中為global,瀏覽器為window,這裡理解可以理解為全域性物件
}

複製程式碼

建立完此環境後,開始進入全域性啟用\執行階段,也就是執行foo(0), 在執行foo(0)之前,根據當前的函式體建立一個新的函式執行上下文,函式執行上下文可以抽象為下面的object:

fooExecutionContextObj1 = {
    scopeChain: { 
        i : 0 /* 變數物件(variableObject)+ 所有父執行上下文的變數物件*/ 
    },  
    variableObject: {  
        foo: [function] //
    }, /*函式 arguments/引數,內部變數和函式宣告 */
    this: window  // 在函式體內this指向呼叫父函式的物件,這裡this代表為window
}
複製程式碼

函式執行上下文fooExecutionContextObj1建立完成後,進行啟用/執行階段,在一次進入foo函式體內部,不過此時建立的執行上下文fooExecutionContextObj2中,scopeChain.i 變成了2,js引擎重複上面的過程,到第三次啟用執行階段,return 語句結束,壓在棧頂的第三次的foo函式被推出,接著第二次壓入的foo函式推出,最終執行棧回退到全域性執行上下文,一直到程式碼結束,因此上面的程式碼最終列印出來的分別是 2,1,0

解釋變數提升現象

在進入啟用執行階段之前,引擎會先建立執行環境,因此,並不是按照程式碼的書寫順序來理解執行,如下栗子中:

    console.log(foo); // 函式指標
    console.log(bar); // undefined

    var foo = 'hello';
    var bar = function() {
        return 'world';
    };

    function foo() {
        return 'hello';
    }
複製程式碼

使用function 關鍵字生命的foo,覆蓋了前面宣告的foo便令,會被先新增到執行環境中的variableObject,但bar變數只會進行宣告,它的值只有在執行後才會被解析,因此foo可以列印出foo的函式體,bar列印出為undefined

執行上下文中this的四種含義

this在執行上下文中才會被明確,因此js程式碼是動態的,不能以一般靜態語言的邏輯對它進行推測分析,在《javascript語言精粹》中列舉的this指標可能的四種傳值方式。

  • 在普通函式中呼叫
  • 作為物件的方法
  • 在建構函式中呼叫
  • 使用Function.prototype.bind、Function.prototype.call、Function.prototype.apply方法改變this指向

執行上的下文與程式碼應用

作用域

閉包

函式柯里化

柯里化通常也被稱為部分求值,它的含義是給函式分步傳遞引數,每次傳遞引數後,部分應用引數,並返回一個更具體的函式接受剩下的引數,中間可以巢狀多個這樣的過程,逐步縮小適用範圍,逐步求解,直至返回最終結果

js 中Function.prototype.bind方法實現了柯里化,看如下程式碼:

Function.prototype.bind = function(ctx) {
    var fn = this;
    return function() {  // 返回的是一個function,先把引數存起來,實際並沒有呼叫
        fn.apply(ctx, arguments);
    };
};
複製程式碼

總結

  1. 執行上下文: 在函式執行前被建立,可以用一個object進行抽象描述,分別包含,作用域鏈,當前環境變數,this指標
  2. 執行上下文解釋了變數提升的現象
  3. this指標在建立時根據不同情形,它的具體值可能有四種情況:
  • 作為物件的方法呼叫,指向這個物件。
  • 在普通函式體中直接呼叫,此時指向全域性(原書中說道,這裡應該被設計成指向上一級執行環境,是設計的糟粕)。
  • 在建構函式中呼叫,指向新構造出來的物件示例。
  • 使用apply, call, bind時,指向被繫結的物件。

相關文章