执行上下文

执行上下文(Execution Context,EC),指的是代码的运行环境。一段代码什么时候运行,运行结果是什么,取决于代码当前所处的运行环境,也就是执行上下文。

分类

在 JavaScript 中,执行上下文主要分为两类:

  • 全局上下文:所有全局代码的默认运行环境。
  • 局部上下文:函数内部代码的运行环境。

生命周期

执行上下文的生命周期的分为三个阶段:

  1. 执行上下文创建阶段:全局上下文是在浏览器打开时创建,局部上下文是在函数调用时创建;
  2. 代码执行阶段:执行上下文创建成功后,就开始进入代码执行阶段,即开始运行当前上下文中的所有代码;
  3. 执行上下文销毁阶段:待当前上下文中所有代码运行完成后,该上下文销毁;

执行上下文栈

在 JavaScript 中,会利用执行上下文栈(Execution Context Stack,ECS)来管理所有的执行上下文。

每当有一个执行上下文创建出来,就会保存进栈内存中,这个过程我们称为“压栈”。待到某一个上下文中的代码执行完毕,该上下文就会从栈内存中销毁,这个过程我们称为“出栈”。

《JavaScript 内存空间》中我们知道,栈内存的存储方式是“先进后出,后进先出”。也就是说,执行上下文在栈内存中的压栈出栈也要遵循这个规则。

所以执行上下文压栈是从下往上压栈,即最先进入栈空间的执行上下文会被保存在最底部,最后进入栈空间的执行上下文会被保存在最顶部。而执行上下文出栈是从上往下出栈,即只能从最顶层开始往下销毁。

又因为 JavaScript 是单线程语言。单线程放在现实生活就表现为同一时间内只能做一件事。那么,在栈内存中,单线程就体现在:同一时间内处于活动状态的永远都只能是栈内存中最顶层的执行上下文

(活动状态即指可以进入执行上下文的代码执行阶段)

我们通过一段代码来具体看一下执行上下文的压栈出栈过程。

1
2
3
4
5
6
7
function outer(){
function inner(){
console.log( "inner" );
}
inner();
}
outer();

第一步:全局上下文压栈。

全局上下文压栈后,全局上下文进入活动状态,其内部可执行代码开始执行。执行到第 7 行代码outer()时,函数outer被调用,激活函数outer创建它自己的执行上下文。

第二步:函数outer的执行上下文压栈。

outer的上下文压栈后,outer的上下文进入活动状态,outer函数内部的可执行代码开始执行。执行到第 5 行代码inner()时,函数inner被调用,激活函数inner创建它自己的执行上下文。

第三步:函数inner的执行上下文压栈。

inner的上下文压栈后,inner的上下文进入活动状态,inner函数内部的可执行代码开始执行。然后没有再遇到其他能生成执行上下文的情况,因此函数inner内的代码顺利执行完毕,inner的上下文销毁出栈。

第四步:函数inner的执行上下文出栈。

inner的上下文销毁之后,函数outer的执行上下文重新回到活动状态,继续执行其内部可执行代码,然后也没有再遇到其他能生成执行上下文的情况,因此函数outer内的代码也顺利执行完毕,outer上下文销毁出栈。

第五步:函数outer的执行上下文出栈。

outer的上下文销毁之后,全局上下文重新回到活动状态,继续执行其内部可执行代码,然后也没有再遇到其他能生成执行上下文的情况。直到浏览器关闭,全局上下文销毁出栈。

注意:在函数中,如果遇到return会直接终止函数内可执行代码的执行,因此函数调用也会立即完成,所以函数的上下文也会立即从栈空间中销毁。

练习:

为了巩固对执行上下文的理解,我们最后再来分析一个代码例子。

1
2
3
4
5
6
7
8
function foo(){
function bar(){
console.log( 'hello' );
}
return bar;
}
var result = foo();
result(); // hello

函数bar在函数foo中,并没有被调用执行。因此,执行foo的时,bar不会创建新的上下文,而是直到result执行时,才创建了一个新的。具体过程如下:

总结

了解完整个过程之后,我们就可以对执行上下文总结一些结论了。

  • 处于活动状态的永远都只能是最顶层的执行上下文。只要栈顶的上下文中的代码处于执行中,其他上下文都需要等待。
  • 全局上下文只有唯一的一个,在浏览器打开时创建入栈,在浏览器关闭时销毁出栈。
  • 局部上下文个数没有限制,在每次函数被调用时创建入栈,函数执行完成后销毁出栈。
  • 同一个函数被调用多少次,就会产生多少执行上下文,即使是自己调用自己。
我 秦始皇 打钱