ECMAScript执行上下文
作用域、调用栈、提升与执行上下文之间的运行时关系。
#type / concept
#status / growing
#resource / javascript
#resource / ecmascript
[!info] related notes
- 所属 MOC: ecmascript-moc, ecmascript
- 相邻主题: ecmascript-functions, ecmascript-closures, ecmascript-syntax-basics, this-keyword
- 关系辨析: 作用域和执行上下文的区别, 调用栈和执行上下文的关系
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] // inner 执行完弹出
5. [GlobalContext] // outer 执行完弹出
每层上下文只能访问自己和外层的变量,不能访问同级或内层的。
提升与暂时性死区
提升不是代码真的被搬到顶部,而是声明绑定在上下文创建阶段就已经处理。
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
}
TDZ 从块开始到 let / const 声明之间的区域,任何访问都会抛错。
this 绑定
this 的值在执行上下文创建阶段确定,取决于函数的调用方式:
const obj = {
name: 'foo',
greet() {
console.log(this.name)
}
}
obj.greet() // "foo"(隐式绑定)
const fn = obj.greet
fn() // undefined(默认绑定,严格模式下也是 undefined)
obj.greet.call({ name: 'bar' }) // "bar"(显式绑定)
箭头函数没有自己的 this,它继承外层执行上下文的 this:
const obj = {
name: 'foo',
greet: () => {
console.log(this.name) // 继承外层 this(通常是全局或外层函数)
}
}
词法作用域为什么重要
变量能不能被访问,首先由定义位置决定,而不是调用位置决定。这也是闭包成立的前提。
function makeCounter() {
let count = 0
return function() {
return ++count
}
}
const counter = makeCounter()
counter() // 1
counter() // 2
返回的函数记住了 makeCounter 执行上下文中的 count,即使 makeCounter 已经执行完毕。
延伸阅读:ecmascript-closures
边界与常见误解
- 执行上下文不是作用域,但它们紧密关联。上下文包含作用域链信息。
var的提升是创建并初始化为undefined;let/const只是创建,不初始化,所以 TDZ 会报错。- 全局上下文的
this在浏览器中是window,在 Node.js 中是global(或globalThis)。 - 严格模式下,函数独立调用时
this是undefined,而非全局对象。 - 每次函数调用都创建新上下文,递归也不例外。
这篇和函数笔记怎么分工
- 看函数定义、参数、
call/apply/bind: ecmascript-functions - 看执行时环境、提升、调用栈: 当前这篇