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的执行上下文百思不解。做者也是花了好久的时间,阅读不少相关的书籍和文章,在加上一些实践才梳理出来这篇文章,但愿能给你们一些帮助,若是文中描述有误,还但愿不吝赐教,提出宝贵的意见和建议。

文末

若是这篇文章有帮助到你,❤️关注+点赞+收藏+评论+转发❤️鼓励一下做者

文章公众号首发,关注不知名宝藏女孩第一时间获取最新的文章

笔芯❤️~