Description
JS深入浅出 - 执行上下文 (Excution Context)
参考链接:
理解JavaScript的执行上下文
[译] 理解 JavaScript 中的执行上下文和执行栈
【你不知道的JavaScript】(三)执行上下文及其生命周期
★ JavaScript深入之执行上下文栈
1. 概念
执行上下文,可以通俗理解为当前正在执行代码所处的执行环境。
分类
执行上下文分为三类:
- 全局执行上下文 [开始运行脚本时创建]
- 函数执行上下文 [调用函数时创建]
- Eval 执行上下文(不常碰到,后续提及上下文均指以上两种)
三个属性
执行上下文用代码可表示为一个对象,其包含三个重要的属性:
两个阶段(生命周期)
- 创建阶段
1.1 生成变量对象
1.2 创建作用域链
1.3 绑定 this 指向 - 执行阶段
2.1 变量赋值
2.2 函数引用
2.3 执行其他代码
一个结构
JavaScript 是单线程,这就意味着代码需要排队按顺序运行,但代码实际运行过程中,并不会按照代码书写方式从上至下一行一行地运行,而是一块一块地运行代码。这里提到的块级结构就是上述提到的“执行上下文”,而执行上下文运行的顺序,是由 JS 引擎维护的一个栈结构管理的,我们称之为“执行上下文栈”。
执行上下文栈(Execution context stack,ECS)
执行上下文栈,顾名思义,它是一个栈结构,在 JS 代码中用数组 []
来表示。
ECStack = [];
在 JS 引擎开始运行脚本时,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext
表示它。全局执行上下文只有当整个应用程序结束的时候,才会被 ECStack 弹出,所以程序结束之前, ECStack 最底部永远有个 globalContext
。
ECStack = [globalContext]
执行上下文栈管理执行上下文的方式就是按函数调用顺序,将函数调用时产生的函数执行上下文压入栈顶,将栈顶已运行完毕的函数执行上下文弹出,弹出的函数执行上下文等待被垃圾回收机制回收。
假设 JS 正在运行以下代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
ECStack 内变化如下所示:
// 伪代码
// 调用了 fun1(),产生 fun1 执行上下文,将其压入栈顶。
ECStack.push(<fun1> functionContext);
// 开始执行 fun1 上下文,执行过程中 fun1 调用了 fun2。
// 创建 fun2 的执行上下文,将其压入栈顶。
ECStack.push(<fun2> functionContext);
// 暂停 fun1 的执行,开始执行 fun2 上下文。
// fun2 调用了 fun3,同上处理。
ECStack.push(<fun3> functionContext);
// fun3 执行完毕,弹出栈顶。
ECStack.pop();
// fun2执行完毕,弹出栈顶。
ECStack.pop();
// fun1执行完毕,弹出栈顶。
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
我们可以发现,fun2 执行上下文创建和压入栈顶的时机是在 fun1 上下文的执行阶段。这也证实了函数执行上下文是在函数被调用时才会产生。当 fun2 执行上下文被压入栈顶后,fun1 上下文的执行阶段就会被暂停,运行 fun2 的上下文,这也是正确的,可参考下例:
function fun1 () {
var i = 2;
function fun2 () {
console.log(i)
}
fun2();
}
function fun3 () {
function fun4 () {
console.log(i)
}
fun4();
var i = 2;
}
fun1();
fun3();
fun1 结果输出:2
fun3 结果输出:undefined
产生上述结果差异的原因就在于:内部函数执行上下文入栈是在其父级函数执行上下文的执行阶段发生的。
过程分析:
- JS 运行脚本,全局执行上下文入栈。
ECStack = [globalContext]
- 调用
fun1()
,创建fun1()
函数执行上下文。
VO = {
arguments = {
length: 0
},
i: undefined,
fun2: <function-object>
}
- 构建作用域链。
Scope = [VO, globalVO]
- 绑定 this 指向。
- 进入 fun1 上下文执行阶段,运行到变量赋值语句时,修改变量对象内的属性值。
AO = {
arguments = {
length: 0
},
i: 2,
fun2: <function-object>
}
- 调用
fun2()
,开始创建fun2()
函数执行上下文。 - 执行
fun2()
函数上下文,遇到console.log(i)
,在当前上下文没有找到 i,因此沿着作用域链向上查找,在fun1
的变量对象中找到了i === 2
,因此输出 2。
fun3 执行过程同上,只是由于
fun4()
在被调用时,此时 fun3 的var i = 2
还未被执行就被暂停了,因此变量对象中i === undefined
,最终输出undefined
。
还有一个很有意思的点,一个函数被调用时,JS 引擎是从其父级变量对象中对函数进行引用的,看例子!
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
上例中,调用 foo()
时,我们怎么确定调用的是哪个 foo 定义呢?JS 引擎是这样执行的,创建全局上下文入栈,执行全局上下文,此时在执行阶段会运行 foo()
函数引用,JS 引擎会去变量对象中查找,结合变量对象的创建规则,我们知道函数声明会覆盖已存在的同名变量,因此 JS 引擎会从变量对象中查找到第二个函数定义,并执行它。
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
上例中,由于变量对象在扫描变量声明时,已存在的变量声明不会被覆盖,因此第二个 foo 变量声明实际会被跳过,而在创建阶段的变量对象中,实际只会存在 foo: undefined
。在执行阶段,foo 首先遇到打印 foo1 的赋值语句,然后执行 foo()
,然后遇到打印 foo2 的语句,修改 foo 变量,然后再执行第二个 foo()
。