JavaScript 执行上下文
KongHou

深入理解 JavaScript 执行上下文(Execution Context)

一、前言

在学习 JavaScript 的过程中,你一定遇到过这些问题:

  • 为什么变量会“提升”?
  • 为什么 this 有时候指向全局,有时候又是对象?
  • 为什么闭包中的变量不会被销毁?

这些问题的本质,都离不开 执行上下文(Execution Context) 的机制。
本文将系统地梳理执行上下文的概念,深入理解变量对象、作用域链、执行上下文栈等核心组成部分。


二、执行上下文是什么?

执行上下文(Execution Context,简称 EC)是 JavaScript 代码运行的环境

每当 JavaScript 代码运行一段可执行代码时(例如全局代码、函数代码、eval),引擎就会为其创建一个新的执行上下文,用来存储执行该代码所需的变量、函数声明、作用域链和 this 值等信息。

执行上下文主要分为三种类型:

类型 说明
全局执行上下文(Global EC) 程序开始运行时创建的唯一上下文。windowglobal 对象即为其变量对象。
函数执行上下文(Function EC) 每次函数调用时都会创建一个新的执行上下文。
Eval 执行上下文(Eval EC) eval() 代码执行时创建,一般不推荐使用。

三、执行上下文的创建阶段

执行上下文的创建分为两个阶段:

  1. 创建阶段(Creation Phase)
  2. 执行阶段(Execution Phase)

以函数执行为例,创建阶段会完成以下三个步骤 👇:

1️⃣ 创建变量对象(Variable Object,VO)

变量对象是一个特殊的对象,用来存放:

  • 函数的形参
  • 函数声明
  • 变量声明(但初始值为 undefined

在函数中,变量对象也被称为活动对象(Activation Object, AO)

1
2
3
4
5
function test(a) {
var b = 2;
function c() {}
}
test(1);

创建阶段中,test 的变量对象大致如下:

1
2
3
4
5
VO = {
arguments: { a: 1 },
b: undefined,
c: reference to function c
}

可以看到,函数声明被整体提升,变量声明被提升但未赋值


2️⃣ 建立作用域链(Scope Chain)

每个执行上下文都有一个作用域链,用于解析标识符(变量、函数名)。

作用域链是由当前执行上下文的 词法环境(Lexical Environment) 与所有外层环境的引用组成的一个链表结构。

简单来说:

当我们在函数中访问一个变量时,JavaScript 引擎会沿着作用域链从内到外查找,直到找到该变量或报错为止。

例如:

1
2
3
4
5
6
7
8
9
10
var a = 'global';
function outer() {
var b = 'outer';
function inner() {
var c = 'inner';
console.log(a, b, c);
}
inner();
}
outer();

inner 的作用域链如下:

1
inner → outer → global

当执行 console.log(a, b, c) 时,引擎会:

  1. inner 中找 a,没有;
  2. outer 中找 a,没有;
  3. global 中找到 a = "global"

3️⃣ 确定 this 指向

在执行上下文被创建时,还会确定当前 this 的绑定规则:

  • 默认(非严格模式)下:this 指向全局对象;
  • 严格模式下:thisundefined
  • 若通过 call / apply / bind 调用,则由显式绑定确定;
  • 若作为对象方法调用,则 this 指向该对象。

四、执行阶段(Execution Phase)

创建阶段完成后,JavaScript 开始逐行执行代码,此时:

  • 变量赋值;
  • 函数调用;
  • 表达式运算;
  • 执行上下文栈的入栈与出栈操作。

五、执行上下文栈(Execution Context Stack)

为了管理多个执行上下文,JavaScript 引擎使用一个 执行上下文栈(ECS) 来维护它们的执行顺序。

栈结构示意

1
2
3
4
5
ECStack = [
GlobalEC, // 全局上下文,始终在底部
FunctionEC, // 当前正在执行的函数上下文
...
]

示例:

1
2
3
4
5
6
7
8
9
var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkscope()();

栈变化过程:

1️⃣ 程序启动 → GlobalEC 入栈
2️⃣ 调用 checkscope() → 创建并入栈 checkscopeEC
3️⃣ 执行 checkscope(),返回函数 f(闭包形成) → checkscopeEC 出栈,但其词法环境仍被 f 引用
4️⃣ 执行 f()fEC 入栈,沿作用域链找到 scope = "local scope"
5️⃣ 执行完毕 → fEC 出栈,结果为 "local scope"


六、闭包与执行上下文的关系

闭包(Closure)本质上就是函数+定义时的词法环境的组合。
当一个函数在其外部作用域被返回或传递时,它依然能“记住”定义时的变量。

1
2
3
4
5
6
7
8
9
10
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

这里 makeCounter 的执行上下文虽然销毁了,但它的词法环境被内部函数引用,因此 count 变量依然存在。


七、总结

概念 说明
执行上下文(EC) JavaScript 代码执行时的环境
变量对象(VO) 存储函数参数、变量声明、函数声明
作用域链(Scope Chain) 由当前词法环境与外层环境组成的链,用于变量查找
执行上下文栈(ECS) 管理执行顺序的栈结构
闭包(Closure) 函数与其定义时作用域的绑定

八、思维导图(文字版)

1
2
3
4
5
6
7
8
9
10
11
12
13
执行上下文

├── 全局执行上下文
│ └── 全局对象、this、全局变量

├── 函数执行上下文
│ ├── 变量对象(VO/AO)
│ ├── 作用域链(Scope Chain)
│ └── this绑定

└── 执行上下文栈
├── 入栈:函数调用
└── 出栈:函数执行完毕

Powered by Hexo & Theme Keep
Total words 23.5k