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

做者:小土豆
博客園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/2436173500265335javascript

前言

在正文開始前,先來看兩個JavaScript代碼片斷。前端

代碼一

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

代碼二

fn1();
fn2();

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

若是你能正確的回答解釋以上代碼的輸出結果,那說明你對JavaScript執行上下文已經有必定的瞭解;反之,閱讀完這篇文章,相信你必定會獲得答案。java

什麼是執行上下文

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還會建立一個執行上下文棧用來管理代碼執行過程當中建立的多個執行上下文函數

執行上下文棧也能夠叫作環境棧,在後續的描述中統一簡稱爲執行棧工具

執行棧數據結構中的是同一種數據類型,有着先進後出的特性。post

執行上下文的建立

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

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

建立階段對應到咱們的代碼,也就是代碼剛進入全局環境或者函數剛被調用;而執行階段則對應代碼一行一行在被執行。

建立階段

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

  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的執行上下文百思不解。做者也是花了好久的時間,閱讀不少相關的書籍和文章,在加上一些實踐才梳理出來這篇文章,但願能給你們一些幫助,若是文中描述有誤,還但願不吝賜教,提出寶貴的意見和建議。

文末

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

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

筆芯❤️~

相關文章
相關標籤/搜索