Skip to content

JavaScript 作用域链深度解析

—— 从词法环境到 [[Environment]]

一、什么是作用域链?

定义

作用域链(Scope Chain)是 JavaScript 引擎在查找变量时所遵循的路径
它是由一系列词法环境(Lexical Environments) 通过 [[Environment]] 指针连接而成的链表结构。

核心特性

  • 静态性:在函数定义时就已确定,与调用方式无关(词法作用域)
  • 单向性:只能从内向外查找,不能反向
  • 引用性:通过 [[Environment]] 指针连接,不是拷贝

二、作用域链的构成:[[Environment]] 是关键

1. [[Environment]] 内部属性

每个函数对象在创建时,都会被自动设置一个内部属性:

text
function inner() { /* ... */ }
// 引擎自动设置:
inner.[[Environment]] = outer 的词法环境
  • [[Environment]] 是一个隐式指针,指向函数定义时所在的词法环境。
  • 它不是 JS 代码能直接访问的,但引擎用它构建作用域链。

规范依据ECMA-262: ECMAScript Function Objects

2. 词法环境(Lexical Environment)

词法环境是一个抽象结构,包含:

  • 环境记录(Environment Record):存储变量绑定(如 x: 10
  • 外部环境引用(Outer Environment Reference):指向外层词法环境
js
// 当 outer 执行时,创建的词法环境类似:
{
  record: { x: 10 }
  outer: globalEnvironment
}

3. 作用域链 = [[Environment]]

当多个函数嵌套时,[[Environment]] 形成一条链:

text
inner.[[Environment]] 
    → 指向 outer 的词法环境
        → outer.[[Environment]] 
            → 指向全局词法环境

这就是一个单向链表,每个节点是词法环境,指针是 [[Environment]]

三、变量查找全过程(引擎视角)

我们以经典闭包为例:

js
function outer() {
  let x = 10;
  function inner() {
    console.log(x); // ← 查找 x
  }
  return inner;
}
const fn = outer();
fn(); // 调用 inner

步骤 1:调用 fn(),创建执行上下文

  • 引擎创建 inner 的执行上下文
  • 推入调用栈
  • 初始化 LexicalEnvironment

步骤 2:开始查找变量 x

  1. inner 的词法环境中查找 x
    → ❌ 未找到

  2. 通过 inner.[[Environment]] 跳转到 outer 的词法环境
    → ✅ 找到 x = 10

  3. 返回值,查找结束

✅ 整个过程就是沿着 [[Environment]] 链向上遍历。

图示:作用域链结构(文字版)

调用栈(Call Stack)
┌─────────────────┐
│ inner 执行上下文 │ ← 当前执行
└─────────────────┘

作用域链(Scope Chain)

┌─────────────────────┐
│ inner 的 LexicalEnv │ → 查找 x ❌
└─────────────────────┘
        ↑ [[Environment]]

┌─────────────────────┐
│ outer 的 Context    │ → 查找 x ✅ (x = 10)
└─────────────────────┘
        ↑ [[Environment]]

┌─────────────────────┐
│ 全局 Context        │
└─────────────────────┘

作用域链 ≠ 调用栈
它是词法结构,不是运行时结构

四、常见误解澄清

误解正确理解
“作用域链是调用栈”❌ 作用域链是静态的,调用栈是动态的
“inner 调用 outer 来拿 x”outer 没有被调用,只是访问它留下的环境
“x 被复制给了 inner”❌ 是引用共享,不是值拷贝
“作用域链是数组”❌ 是链表,通过指针连接,不是连续内存

五、V8 引擎如何实现?

1. Context 对象

V8 为被捕获的外层变量创建堆上的 Context 对象:

  • 存储变量绑定(如 x: 10
  • 可被多个闭包共享
  • 通过 [[Environment]] 引用

2. 优化机制

优化说明
上下文折叠如果变量未被捕获,直接分配在栈上,不创建 Context
内联缓存第一次查找后缓存 slot 位置,后续直接访问
隐藏类提升对象属性访问速度

但语义不变:仍然是“沿 [[Environment]] 链查找”

六、垃圾回收与作用域链

  • 只要 inner 存在,inner.[[Environment]] 就引用 outerContext
  • Context 不会被 GC 回收
  • fn = null,引用断开
  • → 下次 GC 时,Context 被回收

风险:若闭包长期存在,会导致内存泄漏

七、最佳实践

建议说明
✅ 使用 const / let避免意外修改共享变量
✅ 避免在循环中创建闭包捕获 varlet 或 IIFE 隔离
✅ 主动断开引用fn = null 帮助 GC
✅ 使用 WeakMap存储私有数据,不阻止 GC

八、面试如何回答?

高级回答模板:

“作用域链本质上是函数与其词法环境之间的引用链。
每个函数在创建时都会通过 [[Environment]] 内部属性记住它定义时的词法环境。
当查找变量时,引擎会从当前函数的作用域开始,沿着 [[Environment]] 指针逐级向上查找,直到全局环境。
这种机制支持了闭包的实现,也决定了变量的访问权限。”

九、延伸阅读