1. 程式人生 > 實用技巧 >JavaScript的執行上下文,真沒你想的那麼難

JavaScript的執行上下文,真沒你想的那麼難

作者:小土豆
部落格園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/2436173500265335

前言

在正文開始前,先來看兩個JavaScript程式碼片段。

程式碼一

console.log(a);
var a = 10;

程式碼二

fn1();
fn2();

function fn1(){
    console.log('fn1');
}
var fn2 = function(){
    console.log('fn2');
}

如果你能正確的回答解釋以上程式碼的輸出結果,那說明你對JavaScript執行上下文

已經有一定的瞭解;反之,閱讀完這篇文章,相信你一定會得到答案。

什麼是執行上下文

var a = 10;

function fn1(){
    console.log(a);     // 10
    function test(){
        console.log('test');
    }
}

fn1();
test();   // Uncaught ReferenceError: test is not defined

上面這段程式碼我們在全域性環境中定義了變數a和函式fn1,在呼叫函式fn1時,fn1內部可以成功訪問全域性環境中定義的變數a;接著,我們在全域性環境中呼叫了fn1

內部定義的test函式,這行程式碼會導致ReferenceError,因為我們在全域性環境中無法訪問fn1內部的test函式。那這些變數或者函式能否正常被訪問,就和JavaScript執行上下文有著很大的關係。

JavaScript執行上下文也叫JavaScript執行環境,它是在JavaScript程式碼的執行過程中創建出來的,它規定了當前程式碼能訪問到的變數函式,同時也支援著整個JavaScript程式碼的執行。

在一段程式碼的執行過程中,如果是執行全域性環境中的程式碼,則會建立一個全域性執行上下文,如果遇到函式,則會建立一個函式執行上下文

如上圖所示,程式碼在執行的過程中建立了三個執行上下文

:一個全域性執行上下文,兩個函式執行上下文。因為全域性環境只有一個,因此在程式碼的執行過程中只會建立一個全域性執行上下文;而函式可以定義多個,所以根據程式碼有可能會建立多個函式執行上下文

同時JavaScript還會建立一個執行上下文棧用來管理程式碼執行過程中建立的多個執行上下文

執行上下文棧也可以叫做環境棧,在後續的描述中統一簡稱為執行棧

執行棧資料結構中的是同一種資料型別,有著先進後出的特性。

執行上下文的建立

前面我們簡單理解了執行上下文的概念,同時知道了多個執行上下文是通過執行棧進行管理的。那執行上下文如何記錄當前程式碼可訪問的變數函式將是我們接下來需要討論的問題。

首先我們需要明確執行上下文生命週期包含兩個階段:建立階段執行階段

建立階段對應到我們的程式碼,也就是程式碼剛進入全域性環境或者函式剛被呼叫;而執行階段則對應程式碼一行一行在被執行。

建立階段

執行上下文建立階段會做三件事:

  1. 建立變數物件(Variable Object,簡稱VO)
  2. 建立作用域鏈(Scope Chain)
  3. 確定this指向

this想必大家都知道,那變數物件作用域鏈又是什麼呢,這裡先給大家梳理出這兩個的概念。

變數物件: 變數物件儲存著當前環境可以訪問的變數函式,儲存方式為key:value,其中key為變數名或者函式名,value為變數的值或者函式引用。

作用域鏈作用域鏈是由變數物件組成的一個列表或者連結串列結構,作用域鏈的最前端是當前環境的變數物件作用域的下一個元素是上一個環境變數物件,再下一個元素是上上一個環境的變數物件,一直到全域性的環境中的變數物件全域性環境變數物件始終是作用域鏈的最後一個物件。當我們在一段程式碼中訪問某個變數或者函式時,會在當前環境的執行上下文的變數物件中查詢變數或者函式,如果沒有找到,則會沿著作用域鏈一直向下查詢變數函式

這裡的描述的環境無非兩種,一種是全域性的環境,一種是函式所在的環境。

此處參考《JavaScript高階程式設計》第三版第4章2節。

相信很多人此刻已經沒有信心在往下看了,因為我已經丟擲了好多的概念:執行上下文執行上下文棧變數物件作用域鏈等等。不過沒有關係,我們不用太過於糾結這些所謂的名詞,以上的內容大致有個印象即可,繼續往下看,疑惑會慢慢解開。

全域性執行上下文

我們先以全域性環境為例,分析一下全域性執行上下文建立階段會有怎樣的行為。

前面我們說過全域性執行上下文建立階段對應程式碼剛進入全域性環境,這裡為了模擬程式碼剛進入全域性環境,我在JavaScript指令碼最開始的地方打了斷點

<script>debugger
    var a = 10;
    var b = 5;
    function fn1(){ 
        console.log('fn1 go')
    }
    function fn2(){
        console.log('fn2 go')
    }
    fn1();
    fn2();
</script>

這種除錯方式可能不是很準確,但是可以很好的幫助我們理解抽象的概念。

執行這段程式碼,程式碼執行到斷點處會停下來。此時我們在瀏覽器console工具中訪問我們定義的變數函式

可以看到,我們已經能訪問到var定義的變數,這個叫變數宣告提升,但是因為程式碼還未被執行,所以變數的值還是undefined;同時宣告的函式也可以正常被呼叫,這個叫為函式宣告提升

前面我們說變數物件儲存著當前環境可以訪問到的變數函式,所以此時變數物件的內容大致如下:

// 變數物件
VO:{
    a: undefined,
    b: undefined,
    fn1: <Function fn1()>,  // 已經是函式本身 可以呼叫
    fn2: <Function fn2()>   // 已經是函式本身 可以呼叫
},

此時的this也已經指向window物件。

所以this內容如下:

//this儲存的是window物件的地址,即this指向window 
this: <window Reference> 

最後就是作用域鏈,在瀏覽器的斷點除錯工具中,我們可以看到作用域鏈的內容。

展開Scope項,可以看到當前的作用域鏈只有一個GLobal元素,Global右側還有一個window標識,這個表示Global元素的指向是window物件。

// 作用域鏈
scopeChain: [Global<window>],   // 當前作用域鏈只有一個元素

到這裡,全域性執行上下文建立階段中的變數物件作用域鏈this指向梳理如下:

// 全域性執行上下文
GlobalExecutionContext = {
    VO:{
    	a: undefined,
        b: undefined,
        fn1: <Function fn1()>,  // 已經是函式本身 可以呼叫
        fn2: <Function fn2()>   // 已經是函式本身 可以呼叫
    },
    scopeChain: [Global<window>],  // 全域性環境中作用域鏈只有一個元素,就是Global,並且指向window物件
    this: <window Reference>    // this儲存的是window物件的地址,即this指向window

}

前面我們說作用域鏈是由變數物件組成的,作用域鏈的最前端是當前環境的變數物件。那根據這個概念,我們應該能推理出來:GlobalExecutionContext.VO == Global<window> == window的結果為true,因為GlobalExecutionContext.VOGlobal<window>都是我們虛擬碼中定義的變數,在實際的程式碼中並不存在,而且我們也訪問不到真正的變數物件,所以還是來看看瀏覽器中的斷點除錯工具。

我們展開Global選項。

可以看到Global中是有我們定義的變數ab和函式fn1fn2。同時還有我們經常會用到的變數document函式alertconform等,所以我們會說Global是指向window物件的,這裡也就能跟瀏覽器的顯示對上了。

最後就是對應的執行棧

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]

函式執行上下文

此處參考全域性上下文,在fn1函式執行前打上斷點

<script>
    var a = 10;
    var b = 5;
    function fn1(param1, param2){ debugger
        var result = param1 + param2;
        function inner() {
            return 'inner go';
        }
        inner();
        return 'fn1 go'
    }
    function fn2(){
        return 'fn2 go'
    }
    fn1(a,b);
    fn2();
</script>

開啟瀏覽器,程式碼執行到斷點處暫停,繼續在console工具中訪問一些相關的變數函式

根據實際的除錯結果,函式執行上下文變數物件如下:

其實在函式執行山下文中,變數物件不叫變數物件,而是被稱之為活動物件(Active Object,簡稱AO),它們其實也只是叫法上的區別,所以後面的虛擬碼中,我統一寫成VO
但是這裡有必要給大家做一個說明,以免造成一些誤解。

// 變數物件
VO: {
    param1: 10,
    param2: 5,
    result: undefined,
    inner: <Function inner()>,
    arguments:{
    	0: 10,
        1:5,
        length: 2,
        callee: <Function fn1()>
    }
}

對比全域性的執行上下文函式執行上下文變數物件除了函式內部定義的變數函式,還有函式的引數,同時還有一個arguments物件。

arguments物件是所有(非箭頭)函式中的區域性變數,它和函式的引數有著一定的對應關係,可以使用從arguments中獲得函式的引數。

函式執行上下文作用域鏈如下:

用程式碼表示:

// 作用域鏈
scopeChain: [
    Local<fn1>,     // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO
    Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
] 

作用域鏈最前端的元素是Local,也就是當前環境當前環境就是fn1函式)的變數物件。我們可以展開Local,其內容基本和前面我們總結的變數物件VO一致。

這個Local展開的內容和前面總結的活動物件AO基本一致,這裡只是Chrome瀏覽器的展示方式,不用過多糾結。

this物件同樣指向了window

fn1函式內部的this指向window物件,源於fn1函式的呼叫方式。

總結函式執行上下文建立階段的行為:

// 函式執行上下文
Fn1ExecutionContext = {
    VO: {
        param1: 10,
        param2: 5,
        result: undefined,
        inner: <Function inner()>,
        arguments:{
            0: 10,
            1:5,
            length: 2,
            callee: <Function fn1()>
        }
    },
    scopeChain: [
        Local<fn1>,  // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO
        Global<window> // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

此時的執行棧如下:

// 執行棧
ExecutionStack = [
    Fn1ExecutionContext,      // fn1執行上下文
    GlobalExecutionContext    // 全域性執行上下文
]

執行階段

執行上下文執行階段,相對來說比較簡單,基本上就是為變數賦值和執行每一行程式碼。這裡以全域性執行上下文為例,梳理執行上下文執行階段的行為:

// 函式執行上下文
Fn1ExecutionContext = {
	VO: {
            param1: 10,
            param2: 5,
            result: 15,
            inner: <Function inner()>,
            arguments:{
                0: 10,
                1:5,
                length: 2,
                callee: <Function fn1()>
            }
    	},
        scopeChain: [
            Local<fn1>,  // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO
            Global<window> // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
        ],
        this: <window reference>
}

執行上下文的擴充套件

堅持看到這裡的同學,相信大家對JavaScript的執行上下文已經有了一點的認識。那前面為了讓大家更好的理解JavaScript的執行上下文,我省略了一些特殊的情況,那接下來緩口氣,我們在來看看有關執行上下文的更多內容。

let和const

ES6特性熟悉的同學都知道ES6新增了兩個定義變數的關鍵字letconst,並且這兩個關鍵字不存在變數宣告提升

還是前面的一系列除錯方法,我們分析一下全域性環境中的letconst。首先我們執行下面這段JavaScript程式碼。

<script> debugger
    let a = 0;
    const b = 1;
</script>

斷點處訪問變數ab,發現出現了錯誤。

那這個說明在執行上下文執行階段,我們是無法訪問letconst定義的變數,即進一步證實了letconst不存在變數宣告提升。也說明了在執行上下文建立階段變數物件中沒有letconst定義的變數。

函式

函式一般有兩種定義方式,第一種是函式宣告,第二種是函式表示式

// 函式宣告
function fn1(){
    // do something
}

// 函式表示式
var fn2 = function(){
    // do something
}

接著我們來執行下面的這段程式碼。

<script> debugger
    function fn1(){
        return 'fn1 go';
    }

    var fn2 = function (){
        return 'fn2 go';
    }
</script>

程式碼執行到斷點處暫停,手動呼叫函式:fn1fn2

從結果可以看到,對於函式宣告,因為存在函式宣告提升,所以可以在函式定義前使用函式;而對於函式表示式,在函式定義前使用會導致錯誤,說明函式表示式不存在函式宣告提升

這個例子補充了前面的內容:在執行上下文建立階段變數物件的內容不包含函式表示式

詞法環境

在梳理這篇文章的過程中,看到很多文章提及到了詞法環境變數環境這個概念,那這個概念是ES5提出來的,是前面我們所描述的變數物件作用域鏈的另一種設計和實現。基於ES5新提出來這個概念,對應的執行上下文表示也會發生變化。

// 執行上下文
ExecutionContext = {
    // 詞法環境
    LexicalEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { },
        // 外部環境引用
        outer: <outer reference>
    },
    // 變數環境
    VariableEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { },
        // 外部環境引用
        outer: <outer reference>
    },
    // this指向
    this: <this reference>
}

詞法環境環境記錄外部環境引用組成,其中環境記錄變數物件類似,儲存著當前執行上下文中的變數函式;同時環境記錄在全域性執行上下文中稱為物件環境記錄,在函式執行上下文中稱為宣告性環境記錄

// 全域性執行上下文
GlobalExecutionContext = {
    // 詞法環境
    LexicalEnvironment: {
        // 環境記錄之物件環境記錄
    	EnvironmentRecord: { 
            Type: "Object"    // type標識,表明該環境記錄是物件環境記錄
        },
        // 外部環境引用
        outer: <outer reference>
    }
}

// 函式執行上下文
FunctionExecutionContext = {
    // 詞法環境
    LexicalEnvironment: {
        // 環境記錄之宣告性環境記錄
    	EnvironmentRecord: { 
            Type: 'Declarative' // type標識,表明該環境記錄是宣告性環境記錄
        },
        // 外部環境引用
        outer: <outer reference>
    }
}

這點就類似變數物件也只存在於全域性上下文中,而在函式上下文中稱為活動物件

詞法環境中的外部環境儲存著其他執行上下文的詞法環境,這個就類似於作用域鏈

除了詞法環境之外,還有一個名詞變數環境,它實際也是詞法環境,這兩者的區別是變數環境只儲存用var宣告的變數,除此之外像letconst定義的變數函式宣告、函式中的arguments物件等,均儲存在詞法環境中

以這段程式碼為例:

var a = 10;
var b = 5;
let m = 10;
function fn1(param1, param2){
    var result = param1 + param2;
    function inner() {
        return 'inner go';
    }
    inner();
    return 'fn1 go'
}
fn1(a,b);

如果以ES5中新提及的詞法環境變數環境概念來表示執行上下文,應該是下面這樣:

// 執行棧
ExecutionStack = [
    fn1ExecutionContext,  // fn1執行上下文
    GlobalExecutionContext,  // 全域性執行上下文
]
// fn1執行上下文
fn1ExecutionContext = {
    // 詞法環境
    LexicalEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { 
            Type: 'Declarative',  // 函式的環境記錄稱之為宣告性環境記錄
            arguments: {
                0: 10,
                1: 5,
                length: 2
            }, 
            inner: <Function inner>
        },
        // 外部環境引用
        outer: <GlobalLexicalEnvironment>
    },
    // 變數環境
    VariableEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { 
            Type: 'Declarative',  // 函式的環境記錄稱之為宣告性環境記錄
            result: undefined,   // 變數環境只儲存var宣告的變數
        },
        // 外部環境引用
        outer: <GlobalLexicalEnvironment>
    }
}
// 全域性執行上下文
GlobalExecutionContext = {
    // 詞法環境
    LexicalEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { 
            Type: 'Object',  // 全域性執行上下文的環境記錄稱為物件環境記錄
            m: < uninitialized >,  
            fn1: <Function fn1>,
            fn2: <Function fn2>
        },
        // 外部環境引用
        outer: <null>   // 全域性執行上下文的外部環境引用為null
    },
    // 變數環境
    VariableEnvironment: {
        // 環境記錄
    	EnvironmentRecord: { 
            Type: 'Object',  // 全域性執行上下文的環境記錄稱為物件環境記錄
            a: undefined,   // 變數環境只儲存var宣告的變數
            b: undefined,   // 變數環境只儲存var宣告的變數
        },
        // 外部環境引用
        outer: <null>   // 全域性執行上下文的外部引用為null
    }
}

以上的內容基本上參考這篇文章:【譯】理解 Javascript 執行上下文和執行棧。關於詞法環境相關的內容沒有過多研究,所以本篇文章就不在多講,後面的一些內容還是會以變數物件作用域鏈為準。

除錯方法說明

關於本篇文章中的除錯方法,僅僅是我自己實踐的一種方式,比如在斷點處程式碼暫停執行,然後我在console工具中訪問變數或者呼叫函式,其實大可以將這些寫入程式碼中。

console.log(a);
fn1();
fn2();
var a = 10;
function fn1(){
    return 'fn1 go';
}
var fn2 = function (){
    return 'fn2 go';
}

在程式碼未執行到變數宣告函式宣告處,都可以暫且認為處於執行上下文建立階段,當變數訪問出錯或者函式調用出錯,也可以得出同樣的結論,而且這種方式也非常的準確。

反而是我這種除錯方法的實踐過程中,會出現很多和實際不符的現象,比如下面這個例子。

前面我們其實給出過正確結論:函式宣告,可以在函式定義前使用函式,而函式表示式不可以。而如果是我這種除錯方式,會發現此時呼叫innerother都會出錯。

其原因我個人猜測應該是瀏覽器console工具的上層實現的原因,如果你也遇到同樣的問題,不必過分糾結,一定要將實際的程式碼執行結果和書中的理論概念結合起來,正確的理解JavaScript執行上下文

躬行實踐

臺下十年功,終於到了臺上的一分鐘了。瞭解了JavaScript執行上下文之後,對於網上流傳的一些高頻面試題和程式碼,都可以用執行上下文中的相關知識來分析。

首先是本文開篇貼出的兩段程式碼。

程式碼一

console.log(a);
var a = 10;

這段程式碼的執行結果相信大家已經瞭然於胸:console.log的結果是undefined。其原理也很簡單,就是變數宣告提升

程式碼二

fn1();
fn2();

function fn1(){
    console.log('fn1');
}
var fn2 = function(){
    console.log('fn2');
}

這個示例應該也是小菜一碟,前面我們已經做過程式碼除錯:fn1可以正常呼叫,呼叫fn2會導致ReferenceError

程式碼三

var numberArr = [];

for(var i = 0; i<5; i++){
    numberArr[i] = function(){
        return i;
    }
}
numberArr[0]();  
numberArr[1]();  
numberArr[2]();  
numberArr[3]();  
numberArr[4]();  

此段程式碼如果刷過面試題的同學一定知道答案,那這次我們用執行上下文的知識點對其進行分析。

step 1

程式碼進入全域性環境,開始全域性執行上下文建立階段

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
    	numberArr: undefined,
        i: undefined,
    },
    scopeChain: [
    	Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 2

接著程式碼一行一行被執行,開始全域性執行上下文執行階段

當代碼開始進入第一個迴圈:

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為1,第一個元素是一個Function
    	numberArr: Array[1][f()], 
        i: 0,
    },
    scopeChain: [
    	Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

上面總結的執行上下文內容是程式碼已經進入到第一個迴圈,跳過了numberArr宣告賦值,後面所有的程式碼只分析關鍵部分,不會一行一行的分析。

step 3

程式碼進入第五次迴圈(第五次迴圈因為不滿足條件並不會真正執行,但是i值已經加1):

省略i=2i = 3i = 4的執行上下文內容。

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為5,元素均為Function
    	numberArr: Array[5][f(), f(), f(), f(), f()],
        i: 5,
    },
    scopeChain: [
    	Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

迴圈部分結束以後,我們發現i此時的值已經是5了。

step 4

接著我們訪問numberArr中的元素numberArr中的每一個元素都是一個匿名函式,函式返回i的值)並呼叫。首先是訪問下標為0的元素,之後呼叫對應的匿名函式,既然是函式呼叫,說明還會生成一個函式執行上下文

// 執行棧
ExecutionStack = [
    FunctionExecutionContext   // 匿名函式執行上下文
    GlobalExecutionContext    // 全域性執行上下文
]
// 匿名函式執行上下文
FunctionExecutionContext = {
    VO: {},    // 變數物件為空
    scopeChain: [
    	LocaL<anonymous>,  // 匿名函式執行上下文的變數物件,即FunctionExecutionContext.VO
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <numberArr reference>   // this指向numberArr this == numberArr 值為true  
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為5,元素均為Function
    	numberArr: Array[5][f(), f(), f(), f(), f()],
        i: 5,
    },
    scopeChain: [
       Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

呼叫匿名函式時,函式執行上下文變數物件的值為空,所以當該匿名函式返回i時,在自己的變數物件中沒有找到對應的i值,就會沿著自己的作用域鏈(scopeChain)去全域性執行上下文的變數物件Global<window>中查詢,於是返回了5

那後面訪問numberArr變數的第1個第2個...第4個元素也是同樣的道理,均會返回5

程式碼四

var numberArr = [];
for(let i = 0; i<5; i++){
    numberArr[i] = function(){
        return i;
    }
}
console.log(numberArr[0]());
console.log(numberArr[1]());
console.log(numberArr[2]());
console.log(numberArr[3]());
console.log(numberArr[4]());

這段程式碼和上面一段程式碼基本一致,只是我們將迴圈中控制次數的變數i使用了let關鍵字宣告,那接下來開始我們的分析。

step 1

首先是全域性執行上下文建立階段

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
    	numberArr: undefined
    },
    scopeChain: [
       Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

因為let關鍵字不存在變數提升,因此全域性執行上下文變數物件中並沒有變數i

step 2

當代碼一行一行的執行,開始全域性執行上下文執行階段

以下是程式碼執行進入第一次迴圈:

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為1,第一個元素是一個Function
    	numberArr: Array[1][f()], 
    },
    scopeChain: [
       Block,           // let定義的for迴圈形成了一個塊級作用域
       Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

可以看到當迴圈開始執行時,因為遇到了let關鍵字,因此會建立一個塊級作用域,裡面包含了變數i的值。這個塊級作用域非常的關鍵,正是因為這個塊級作用域在迴圈的時候儲存了變數的值,才使得這段程式碼的執行結果不同於上一段程式碼。

step 3

i值為5時:

省略i=1i = 3i = 4的執行上下文內容。

GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為2,元素均為Function
    	numberArr: Array[5][f(), f(), f(), f(), f()],
    },
    scopeChain: [
        Block, 
        Global<window>
    ],
    this: <window reference>
}

此時塊級作用域中變數i的值也同步更新為5

step 4

接著就是訪問陣列中的第一個元素,呼叫匿名函式匿名函式在執行的時候會建立一個函式執行上下文


// 執行棧
ExecutionStack = [
    FunctionExecutionContext, // 匿名函式執行上下文
    GlobalExecutionContext    // 全域性執行上下文
]
// 匿名函式執行上下文
FunctionExecutionContext = {
    VO: {},    // 變數物件為空
    scopeChain: [
    	LocaL<anonymous>,  // 匿名函式執行上下文的變數物件,即FunctionExecutionContext.VO
        Block,   // 塊級作用域
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <numberArr reference>   // this指向numberArr this == numberArr 值為true  
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        // 這種寫法代表number是一個Array型別,長度為2,元素均為Function
    	numberArr: Array[5][f(), f(), f(), f(), f()],
    },
    scopeChain: [
        Global<window>
    ],
    this: <window reference>
}

匿名函式因為儲存著let關鍵字定義的變數i,因此作用域鏈中會儲存著第一次迴圈時建立的那個塊級作用域,這個塊級作用域前面我們說過也在瀏覽器的除錯工具中看到過,它儲存著當前迴圈的i值。

所以當return i時,當前執行上下文的變數物件為空,就沿著作用域向下查詢,在Block中找到對應的變數i,因此返回0;後面訪問numberArr[1]()numberArr[2]()、...、numberArr[4]()也是同樣的道理。

程式碼五

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

這段程式碼包括下面的都是在梳理這篇文章的過程中,看到的一個很有意思的示例,所以貼在這裡和大家一起分析一下。

step 1

程式碼進入全域性環境,開始全域性執行上下文建立階段

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: undefined,
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
       Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 2

全域性執行上下文執行階段

// 執行棧
ExecutionStack = [
    GlobalExecutionContext    // 全域性執行上下文
]
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',      // 變數賦值
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
       Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 3

當代碼執行到最後一行:checkscope(),開始checkscope函式執行上下文建立階段

// 執行棧
ExecutionStack = [
    CheckScopeExecutionContext,  // checkscope函式執行上下文
    GlobalExecutionContext    // 全域性執行上下文
]
// 函式執行上下文
CheckScopeExecutionContext = {
    VO: {
        scope: undefined,
        f: <Function f>, // 函式已經可以被呼叫
    },
    scope: [
        Local<checkscope>,    // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>   //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}

// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 4

接著是checkscope函式執行上下文執行階段

// 執行棧
ExecutionStack = [
    CheckScopeExecutionContext,  // 函式執行上下文
    GlobalExecutionContext    // 全域性執行上下文
]
// 函式執行上下文
CheckScopeExecutionContext = {
    VO: {
        scope: 'local scope',  // 變數賦值
        f: <Function f>, // 函式已經可以被呼叫
    },
    scope: [
        Local<checkscope>,    // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>   //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 5

執行到return f()時,進入f函式執行上下文建立階段

// 函式執行上下文的建立階段
FExecutionContext = {
    VO: {},
    scope: [
        Local<f>,    // f執行上下文的變數物件 也就是FExecutionContext.VO
        Local<checkscope>,  // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>  //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}
// 函式執行上下文
CheckScopeExecutionContext = {
    VO: {
        scope: 'local scope',
        f: <Function f>, // 函式已經可以被呼叫
    },
    scope: [
        Local<checkscope>,  // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>   //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

f函式返回scope變數時,當前f執行上下文中變數物件中沒有名為scope的變數,所以沿著作用域鏈向上查詢,發現checkscope執行上下文的變數物件Local<checkscope>中包含scope變數,所以返回local scope

程式碼六

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

這段程式碼和上面的程式碼非常的相似,只不過checkscope函式的返回值沒有直接呼叫f函式,而是將f函式返回,在全域性環境中呼叫了f函式。

step 1

全域性執行上下文建立階段

// 執行棧
ExcutionStack = [
    GlobalExcutionContext
];
// 全域性執行上下文的建立階段
GlobalExecutionContext = {
    VO: {
        scope: undefined,
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 2

全域性執行上下文執行階段

// 執行棧
ExcutionStack = [
   GlobalExcutionContext    // 全域性執行上下文
];

// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',  // 變數賦值
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>  // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO
    ],
    this: <window reference>
}

step 3

當代碼執行到最後一行:checkscope()(),先執行checkscope(),也就是開始checkscope函式執行上下文建立階段


// 執行棧
ExcutionStack = [
    CheckScopeExecutionContext,     // checkscope函式執行上下文
    GlobalExcutionContext           // 全域性執行上下文
]
// checkscope函式執行上下文的建立階段
CheckScopeExecutionContext = {
    VO: {
        scope: undefined,
        f: <Function f>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Local<checkscope>,    // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>   //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}

// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [Global<window>],
    this: <window reference>
}

step 4

接著是checkscope函式執行上下文執行階段

// 執行棧
ExcutionStack = [
    CheckScopeExecutionContext,     // checkscope函式執行上下文
    GlobalExcutionContext           // 全域性執行上下文
]
// checkscope函式執行上下文
CheckScopeExecutionContext = {
    VO: {
        scope: 'local scope',
        f: <Function f>,      // 函式已經可以被呼叫
    },
    scopeChain: [
        Local<checkscope>,    // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO
        Global<window>   //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO
    ],
    this: <window reference>
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
        Global<window>    // 全域性執行上下文的變數物件
    ],
    this: <window reference>
}

step 5

執行到return f時,此處並不同上一段程式碼,並沒有呼叫f函式,所以不會建立f函式的執行上下文,因此直接將函式f返回,此時checkscope函式執行完畢,會從執行棧中彈出checkscope執行山下文

// 執行棧 (此時CheckScopeExecutionContext已經從棧頂被彈出)
ExcutionStack = [
    GlobalExecutionContext  // 全域性執行上下文
];
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [
    	Global<window>      // 全域性執行上下文的變數物件
    ],
    this: <window reference>
}

step 6

step3中,checkscope()()程式碼的前半部分執行完畢,返回f函式;接著執行後半部分(),也就是呼叫f函式。那此時進入f函式執行上下文建立階段

// 執行棧
ExcutionStack = [
    fExecutionContext,     // f函式執行上下文
    GlobalExecutionContext  // 全域性執行上下文
];

// f函式執行上下文
fExecutionContext = {
    VO: {},   // f函式的變數物件為空
    scopeChain: [
        Local<f>,          // f函式執行上下文的變數物件
        Local<checkscope>, // checkscope函式執行上下文的變數物件
        Global<window>,    // 全域性執行上下文的變數物件
    ],
    this: <window reference>
}
// 全域性執行上下文
GlobalExecutionContext = {
    VO: {
        scope: 'global scope',
        checkscope: <Function checkscope>, // 函式已經可以被呼叫
    },
    scopeChain: [Global<window>],
    this: <window reference>
}

我們看到在f函式執行上下文的建立階段,其變數物件為空字典,而其作用域鏈中卻儲存這checkscope執行上下文變數物件,所以當代碼執行到return scope時,在f函式的變數物件中沒找到scope變數,便沿著作用域鏈,在chckscope執行上下文的變數物件Local<checkscope>中找到了scope變數,所以返回local scope

總結

相信很多人和我一樣,在剛開始學習和理解執行山下文的時候,會因為概念過於抽象在加上沒有合適的實踐方式,對JavaScript的執行上下文百思不解。作者也是花了很久的時間,閱讀很多相關的書籍和文章,在加上一些實踐才梳理出來這篇文章,希望能給大家一些幫助,如果文中描述有誤,還希望不吝賜教,提出寶貴的意見和建議。

文末

如果這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下作者

文章公眾號首發,關注不知名寶藏女孩第一時間獲取最新的文章

筆芯❤️~