在上篇文章《作用域》提到了执行上下文(Execution Contexts),以及变量对象(variable object)和作用域链(scope chain)。

js 代码整个执行过程分为代码编译阶段和代码执行阶段。在编译阶段确定作用域,执行阶创建执行上下文

当查找变量时,会先从当前执行上下文变量对象中查找。如果没有找到,就会从父级执行上下文的变量对象中查找,以及父级的父级直到全局执行上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就是作用域链。

个人认为context翻译成上下文有点不好理解,环境还会更容易理解,此文的用简写EC表示。

定义

When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context.

js 编译阶段时会把代码编译成一段一段的可执行代码。当控制器遇到可执行代码就会创建EC,由 js 引擎实现。在程序运行时,可能会产生多个 EC,这些 EC 会被保存在栈中,栈底永远是全局 EC ,而栈顶是当前的 EC,执行完毕后出栈。

将一个数组模拟成 EC 栈:

ECStack = []

可执行代码

可执行代码有三种,分别是全局代码、函数代码、eval代码(这里不做讨论)。

全局代码

全局代码是不被包含在函数内的代码。当执行全局代码时,会创建全局 EC 。

ECStack = [
  GlobalContext
]

进栈后,其中的全局代码才会开始执行。

函数代码

在全局代码被执行时,如果遇到函数,则会再创建一个 EC , 被 push 到 ECStack。

ECStack = [
  FunctionContext
  GlobalContext
]

FunctionContext被创建后,开始执行其中的函数代码。如果其中还有函数,又会再创建 EC,以此类推,多个 EC 会形成一个执行栈。

ECStack = [
  FunctionContext(4)
  FunctionContext(3)
  FunctionContext(2)
  FunctionContext(1)
  GlobalContext
]

每一段函数代码执行完毕,该 EC 就会退出 ECStack,最终只剩下全局 EC,全局 EC在程序结束时(浏览器关闭)也会退出栈。

举个例子

var a = 1
var b = 2

function foo() {
	var c = 3

	fucntion bar() {
		var d = 4
	}

	bar()
}

foo()

a,b,foo(),都是全局代码,当遇到这一段代码时,创建全局 EC ,全局 EC 进栈。

ECStack = [
  globaclContext 
]

执行全局 EC 中的代码,当执行到 foo() 时,函数 foo 创建新的 EC

ECStack = [
  foo FunctionContext
  globaclContext 
]

然后开始执行 foo 中的函数代码,当遇到 bar() 后,又会新建一个 EC

ECStack = [
  <bar> FunctionContext
  <foo> FunctionContext
  globaclContext 
]

这时执行 bar 中的代码,当 bar 中的代码执行完毕时,<bar> FunctionContext出栈

ECStack = [
  <foo> FunctionContext
  globaclContext 
]

以此类推,程序关闭时,全局 EC 出栈。

后记

每个执行上下文都有三个重要属性:

  • 变量对象(variable object)
  • 作用域链(Scope chain)
  • this

将会在之后的文章中详细描写。而其中的变量对象是 ES3 的说法,在 ES5 中换了个说法。

总结

需要注意的时,执行上下文和作用域看似相似,但是还是有明显区别。

作用域在词法解析阶段就已经确定了,而执行上下文是在执行时才创建的。

作用域主要是区分变量的作用范围,而执行上下文主要是规范可执行代码的执行顺序。

参考阅读