执行环境(Execution Context)是 JavaScript 中最为重要的一个概念,其定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
有些文章可能会把 Execution Context 翻译成执行上下文。但是我一直对「上下文」这几个字抱有很大的困惑,所以我沿用了红宝书《JavaScript 高级程序设计(第三版)》对其的翻译,使用 执行环境 这个词进行描述和记忆。
先看下 ECMAScript 2015 规范中对执行环境的定义:
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation. At any point in time, there is at most one execution context that is actually executing code. This is known as the running execution context. A stack is used to track execution contexts. The running execution context is always the top element of this stack. A new execution context is created whenever control is transferred from the executable code associated with the currently running execution context to executable code that is not associated with that execution context. The newly created execution context is pushed onto the stack and becomes the running execution context.
……
Transition of the running execution context status among execution contexts usually occurs in stack-like last-in/first-out manner.
简单地翻译一下:
执行环境是一种规范设备,用于跟踪 ECMAScript 实现对代码的运行时评估。在任何时间点,最多有一个实际正在执行代码的执行环境。这称为运行中执行环境。栈用于跟踪执行环境。运行中的执行环境始终是此栈的顶部元素。
每当控制权从与当前正在运行的执行环境关联的可执行代码转移到与该执行环境不关联的可执行代码时,都会创建一个新的执行环境。新创建的执行环境被压入栈,并成为正在运行的执行环境。
……
执行环境之间正在运行的执行环境状态的转换通常以类似栈的后进/先出方式发生。
从规范的定义可以看到执行环境有以下的特点:
- 在执行一段代码时会新创建一个新的执行环境
- 各个执行环境会被存在一个栈中
- 在切换执行环境时,有两种情况,一种是新增执行环境,即把新增的执行环境压入栈中,第二种是执行环境执行完毕之后,把执行完毕的执行环境弹出栈,再把全县切换到下一层的执行环境
- 每次只有位于栈的顶部的执行环境在执行
思考如下的代码:
1 | function foo() { |
我们看看栈里面发生了什么。首先代码在执行的时候会先把全局执行环境推入栈中,此时栈中是这样的:
1 | ECStack = [ |
当运行 foo
函数时,创建了一个新的执行环境,并把这个新的执行环境推入栈中:
1 | ECStack = [ |
在 foo
函数中运行了 bar
函数,重复了上一步:
1 | ECStack = [ |
当 bar
函数执行完之后,其执行环境会被弹出栈:
1 | ECStack = [ |
当 bar
函数执行完之后,foo
函数没有代码执行了,所以其执行环境也会被弹出栈:
1 | ECStack = [ |
于是只剩下全局执行环境。全局执行环境只有在应用程序退出,例如关闭网页或浏览器时,才会被销毁。
变量对象和作用域链
每个执行环境都有一个与其关联的变量对象(Variable Object),执行环境中定义的所有变量和函数都保存在这个对象中。这是一个内部对象,用代码无法访问,在解析器处理数据时会用到。
当代码在执行环境中执行时,同时也会创建一个作用域链(Scope Chain)。作用域链的功能就是把与当前执行环境有关联的变量对象链接起来,保证对执行环境有权访问的所有变量和函数的有序访问。
另外还有一个概念叫活动对象(Active Object),定义为当前正在执行的执行环境的变量对象,并且只存在与函数中。
当执行函数时,会把当前函数的活动对象当作是变量对象加入到作用域链的最前端,当搜索变量标识符时,就会从作用域链的最前端开始搜索,到全局执行环境的变量对象结束,在搜索过程中如果已经找到标识符,则会马上停止,如果直到全局执行环境的变量对象中都搜索不到,则会报错。
在函数执行之前,必须先创建函数。在函数被创建时,会给函数创建一个内部属性:[[Scope]],该函数可以访问的变量标识符和函数都会保存在这个属性中。这个属性在函数创建时即存在,并且是静态的,因为 JavaScript 使用的是词法作用域模型。直到该函数对象被销毁,这个属性才会被随之销毁。
思考如下代码:
1 | var a = 1 |
当运行代码时,全局执行环境中的变量对象包含有几个变量,在全局执行环境执行的时候,已经创建好 foo
函数和 bar
函数,所以它们的 [[Scope]] 内部属性也已经创建好了:
1 | globalContext = { |
当执行 foo
函数时,创建了 foo
函数的作用域链,并把 foo
的活动对象当作变量对象,加入到作用域链中:
1 | fooContext = { |
此时栈中应该是这样的:
1 | ECStack = [ |
在 foo
函数中又执行了 bar
函数:
1 | ECStack = [ |
当 bar
的执行环境执行代码时,对标识符 b
进行赋值操作,此时会搜索 barContext
的作用域链,先从 bar
函数的变量对象开始搜索,没有搜索到 b
,继续往上搜索,查找到了全局执行环境中的变量对象,发现含有 b
标识符,即对其进行重新赋值操作。
总结
在某一时刻,总有一个执行环境在执行。
在执行一段代码时会新创建一个新的执行环境。
各个执行环境会被存在一个栈中。
在切换执行环境时,有两种情况,一种是新增执行环境,即把新增的执行环境压入栈中,第二种是执行环境执行完毕之后,把执行完毕的执行环境弹出栈,再把全县切换到下一层的执行环境。
每次只有位于栈的顶部的执行环境在执行。
每个执行环境都有一个变量对象与之相关联,全局执行环境的就是 window
,函数执行环境的就是其活动变量。
函数创建时会同时创建内部属性 [[Scope]],用于保存在创建时可访问的变量和函数。
函数执行时会把其变量对象与创建时即定义好的 [[Scope]] 内部属性链接起来,组成作用域链。
参考
深入理解 JavaScript 系列(11):执行上下文(Execution Contexts)
深入理解 JavaScript 系列(12):变量对象(Variable Object)
《JavaScript 高级程序设计(第三版)》