ECMAScript执行上下文
作用域、调用栈、提升与执行上下文之间的运行时关系。
#concept
#growing
#javascript
#ecmascript
[!info] related notes
- 所属 MOC: ECMAScript MOC, ecmascript
- 相邻主题: ECMAScript 函数, ecmascript-closures, ecmascript-syntax-basics, this 关键字
- 关系辨析: 作用域和执行上下文的区别, 调用栈和执行上下文的关系
ECMAScript执行上下文
一句话定义
执行上下文是 JavaScript 引擎在执行代码时创建的运行环境,包含变量绑定、this 指向和作用域链,每次函数调用都会生成一个新的执行上下文。
三个容易混淆的词
- 作用域: 变量在代码里按定义位置形成的可访问边界
- 执行上下文: 一段代码执行时实际创建的运行环境
- 调用栈: 多个执行上下文按调用顺序堆叠起来的结构
常见执行上下文类型
- 全局执行上下文
- 函数执行上下文
- 模块执行上下文
eval 也能形成特殊上下文,但现代代码很少把它当主线理解。
一个函数执行时大致会发生什么
创建阶段
- 确定外层词法环境引用
- 处理参数
- 建立声明绑定
- 准备
this与new.target等运行时信息
执行阶段
- 自上而下运行可执行语句
- 在需要时沿作用域链查找标识符
调用栈可视化
function outer() {
const a = 1
inner()
}
function inner() {
const b = 2
console.log(a + b) // ReferenceError: a is not defined
}
outer()
调用栈变化:
1. [GlobalContext]
2. [GlobalContext, outerContext]
3. [GlobalContext, outerContext, innerContext]
4. [GlobalContext, outerContext] // ReferenceError 从 inner 向外传播
5. [GlobalContext] // outer 也因未捕获异常退出
标识符沿词法环境查找,不沿调用栈查找。本例 inner 定义在 outer 外部,所以看不到 outer 的 a;它抛错后也不会正常继续后面的语句。
提升与暂时性死区
提升不是代码真的被搬到顶部,而是声明绑定在上下文创建阶段就已经处理。
console.log(a) // undefined(var 已创建,未赋值)
var a = 10
console.log(b) // ReferenceError(TDZ)
let b = 20
function声明通常最早可用var会被创建并初始化为undefinedlet/const也会被登记,但在初始化前处于暂时性死区(TDZ)
TDZ 的范围
{
// TDZ 开始
console.log(x) // ReferenceError
// TDZ 结束
let x = 1
console.log(x) // 1
}
对块内的 let/const 绑定,TDZ 从进入该词法环境持续到初始化完成;初始化表达式中读取自身也会抛错,例如 let x = x。
this 绑定
this 的值在执行上下文创建阶段确定,取决于函数的调用方式:
const obj = {
name: 'foo',
greet() {
console.log(this.name)
}
}
obj.greet() // "foo"(隐式绑定)
const fn = obj.greet
fn() // 严格函数中 this 为 undefined,读取 this.name 抛 TypeError;非严格时读取全局 name
obj.greet.call({ name: 'bar' }) // "bar"(显式绑定)
箭头函数没有自己的 this,它继承外层执行上下文的 this:
const obj = {
name: 'foo',
greet: () => {
console.log(this.name) // 使用外层 this;若在 ES 模块顶层定义,这里会因 this 为 undefined 抛错
}
}
词法作用域为什么重要
变量能不能被访问,首先由定义位置决定,而不是调用位置决定。这也是闭包成立的前提。
function makeCounter() {
let count = 0
return function() {
return ++count
}
}
const counter = makeCounter()
counter() // 1
counter() // 2
返回的函数保留包含 count 绑定的词法环境,即使 makeCounter 已返回;不意味着原来的调用栈帧仍然驻留。
延伸阅读:ecmascript-closures
边界与常见误解
- 执行上下文不是作用域,但它们紧密关联。上下文包含作用域链信息。
var的提升是创建并初始化为undefined;let/const只是创建,不初始化,所以 TDZ 会报错。- ES 模块顶层 this 是 undefined;浏览器经典脚本顶层通常是 window,Node.js CommonJS 文件顶层通常是 module.exports,不能统一说成 globalThis。
- 严格模式下,函数独立调用时
this是undefined,而非全局对象。 - 每次函数调用都创建新上下文,递归也不例外。
这篇和函数笔记怎么分工
- 看函数定义、参数、
call/apply/bind: ECMAScript 函数 - 看执行时环境、提升、调用栈: 当前这篇