什么是执行上下文(执行上下文环境)?
先看段代码:
console.log(a); // undefined
var a = 100;
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(foo); // ƒ foo () {}
function foo () {}
console.log(fn); // undefined
var fn = function () {};
console.log(this); // Window {0: global, 1: Window,...}
可以看出JavaScript 引擎在执行一段可执行代码之前,会先进行准备工作(也就是对这段代码进行解析(也可以称为预处理)。这个阶段会根据可执行代码创建相应的执行上下文),这些“准备工作”。其中就包括:
- 变量、函数表达式——变量声明,默认赋值为undefined(变量赋值是在赋值语句执行的时候进行的);
- 函数声明——赋值;
- this——赋值;
这三种数据的准备情况我们称之为“执行上下文(execution context)”或者“执行上下文环境”。
注: 以上代码在全局环境下执行的。
可执行代码
javascript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况:
- 全局执行代码,在执行所有代码前,解析创建全局执行上下文。
- 函数执行代码,执行函数前,解析创建函数执行上下文。
- eval执行代码,运行于当前执行上下文中。
执行上下文栈
执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
其实这是一个压栈出栈的过程——执行上下文栈(Execution context stack,ECS)。
举个例子:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
解析如下:
- JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以 JavaScript 引擎会先解析创建全局执行上下文,然后将全局执行上下文压栈
- 然后遇到函数fun1时,会先解析创建函数fun1的执行上下文,然后将它的执行上下文压栈;
- 在fun1中调用了fun2,创建fun2的执行上下文,将fun2的执行上下文压栈;
- 在fun2中调用了fun3,创建fun3的执行上下文,将fun3的执行上下文压栈;
- fun3函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文fun2
- fun2函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文fun1。
- fun1函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文----全局执行上下文
- 注意,全局执行上下文会一直留在栈底,直到整个应用结束。
下图就是执行上下文环境的变化过程:
利用伪代码表示如下:
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
ECStack = [];
程序结束之前, ECStack 最底部永远有个 globalContext:
ECStack = [
globalContext
];
模拟压栈出栈的过程:
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中调用了fun2,创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// fun2中调用了fun3,创建fun3的执行上下文
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
执行上下文的组成
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每一个执行上下文都由以下三个属性组成:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象
变量对象(Variable object,VO)是与执行上下文相关的数据作用域,存储了在执行上下文中定义的所有变量和函数声明,保证代码执行时对变量和函数的正确访问
全局上下文
对于全局上下文来说,全局上下文中的变量对象就是全局对象
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象其实是一个东西,只是处于执行上下文的不同生命周期; 变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,变量对象(VO)转变为了活动对象(AO),而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
执行过程
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
- 进入执行上下文
- 代码执行
进入执行上下文
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
- 函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined
- 函数声明:
- 由名称和对应值(函数对象(function-object),指向对函数的引用)组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在进入执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
总结:
-
全局上下文的变量对象初始化是全局对象
-
函数上下文的变量对象初始化只包括 Arguments 对象
-
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
-
在代码执行阶段,会再次修改变量对象的属性值
参考资料:
ECMAScript规范-第三版-执行上下文
https://github.com/mqyqingfeng/Blog/issues/5
https://www.cnblogs.com/wangfupeng1988/p/3977924.html