执行上下文

55 阅读5分钟

执行上下文是什么

第二句、第三句输出都是 undefined,说明浏览器在执行 console.log(a) 时,已经知道了 a 是 undefined,但却不知道 a 是10。

一段js代码真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:

第一种情况:

第二种情况:

第一种情况只是对变量进行声明(并没有赋值),而第二种情况直接给 this 赋值。这也是“准备工作”情况要做的事情之一。

 

第三种情况:

“函数表达式”“函数声明” 这两者在“准备工作”时,却是两种待遇。

在“准备工作”中,对待函数表达式就像对待 var a = 10 这样的变量一样,只是声明。而对待函数声明时,却把函数整个赋值了。

总结一下,在“准备工作”中完成了哪些工作:

  • 变量、函数表达式 — 变量声明,默认赋值为 undefined
  • this — 赋值
  • 函数声明 — 赋值

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

如果在函数中,除了以上数据之外,还会有其他数据。先看以下代码:

以上代码展示了在函数体的语句执行之前,arguments 变量和函数的参数都已经被赋值。

函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

 

另外一点,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。具体参考JS-01.词法作用域、作用域链

用一个例子说明一下:

 

总结:

全局代码的上下文环境数据内容为:

普通变量(包括函数表达式)如: var a = 10;声明(默认赋值为undefined)
函数声明,如: function fn() { }赋值
this赋值

如果代码段是函数体,那么在此基础上需要附加:

参数赋值
arguments赋值
自由变量的取值作用域赋值

执行上下文类型

全局执行上下文

默认或基础上下文,执行两件事:创建全局对象(浏览器环境是 window)、设置 this 值等于这个全局对象。一个程序中只会有一个全局执行上下文。

函数执行上下文

每当一个函数被调用时, 都会为该函数创建一个新的上下文,每个函数都有它自己的执行上下文,同一个函数多次调用也会各自产生各自的上下文。

eval执行上下文

执行在 eval 函数内部的代码也会有它属于自己的执行上下文,不做讨论

执行栈

执行栈,也就是“调用栈”,是一种 后进先出 数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。

每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行 结束 时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second(); // 对应图3
  console.log('Again inside first function');
  // 执行结束,对应图5
}
function second() {
  console.log('Inside second function');
  // 执行结束,对应图4
}
first(); // 对应图2
console.log('Inside Global Execution Context');

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。

first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

再看这两段代码:

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

两段代码执行的结果一样,但是有哪些不同呢?

答案就是执行上下文栈的变化不一样。

模拟第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop(<f> functionContext);
ECStack.pop(<checkscope> functionContext);

模拟第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop(<f> functionContext);

checkscope()() 相当于 checkscope() 执行完return变量 f ,再执行 f()

总结

执行上下文可以简单理解为一个对象:

  • 它包含三个部分:
    • 变量对象(VO)
    • 作用域链(词法作用域)
    • this指向
  • 它的类型:
    • 全局执行上下文
    • 函数执行上下文
    • eval执行上下文
  • 代码执行过程:
    • 创建 全局上下文 (global EC)
    • 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数调用时,函数执行上下文 (callee) 被push到执行栈顶层
    • 函数执行上下文被激活,成为 active EC,开始执行函数中的代码,caller 被挂起
    • 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行