Skip to content

再探词法作用域

目标:理解词法作用域不仅是“静态的”,更是 JavaScript 引擎构建作用域链、执行上下文、闭包的底层依据。

一、词法作用域的本质:静态的代码结构

什么是词法作用域?

词法作用域(Lexical Scope) 是指变量和函数的作用域由它们在源代码中的位置决定,而不是在运行时动态决定。

例子:

js
function outer() {
  let x = 10;
  function inner() {
    console.log(x); // 能访问 x
  }
  inner();
}
outer();
  • inner 能访问 x,不是因为它“运行时在 outer 里调用”,而是因为它“写在 outer 里面”。

  • 即使你把 inner 返回出去,在别处调用,它依然能访问 x

  • 所以:词法作用域 = 静态作用域 = 基于代码结构的作用域

二、词法作用域 vs 动态作用域

JavaScript 是词法作用域语言,不是动态作用域。

对比:

类型查找变量的方式例子
词法作用域看函数定义在哪里JS、Go、Java
动态作用域看函数在哪里被调用Bash、某些 Lisp 方言

动态作用域的“反例”(JS 不是这样):

js
let x = "global";

function foo() {
  console.log(x);
}

function bar() {
  let x = "bar";
  foo(); // 如果是动态作用域,应输出 "bar"
}

bar(); // 实际输出 "global" ← 因为 foo 定义在全局,只能访问全局 x

JS 输出 "global",说明它是词法作用域:foo 的作用域在它定义时就固定了。

三、词法作用域如何影响执行?——从代码到执行上下文

词法作用域不是“死的”,它在运行时通过以下机制驱动执行

1. 构建作用域链(Scope Chain)

  • 作用域链是一条由 [[Environment]] 指针连接的词法环境链
  • 每个函数都有 [[Environment]],指向它定义时的词法环境。
  • 变量查找时,沿着这条链向上找。
js
let x = 1;

function outer() {
  let y = 2;
  function inner() {
    let z = 3;
    console.log(x, y, z); // x → 全局, y → outer, z → inner
  }
  inner();
}

作用域链:

inner.[[Environment]] → outer 的词法环境

outer.[[Environment]] → 全局词法环境

                    全局对象(window/global)

词法作用域决定了作用域链的结构

2. 决定 [[Environment]] 的指向

这是最核心的一点!

  • 当函数被创建时,引擎根据词法作用域设置它的 [[Environment]]
  • 这个过程是静态的,发生在解析阶段,而不是执行阶段。
text
function makeFn() {
  let env = "I'm here";
  return function() {
    console.log(env);
  };
}
  • 匿名函数在 makeFn 内部定义 → 它的 [[Environment]] 指向 makeFn 的词法环境。

  • 这个关系在函数创建时就定死了,无论它后来在哪里调用。

  • 词法作用域是 [[Environment]] 的“设计图”

3. 影响执行上下文的创建

当函数执行时,会创建执行上下文,其中:

  • LexicalEnvironment:当前函数的局部作用域。
  • [[Environment]]:指向外层词法环境(由词法作用域决定)。
text
// 执行 inner 时:
{
  LexicalEnvironment: { z: 3 },           // 本地变量
  VariableEnvironment: { z: 3 },
  ThisBinding: ...,
  // 外部查找:
  [[Environment]]: 指向 outer 的词法环境 → 可以访问 y
}

所以:词法作用域决定了执行上下文的“外部链接”

四、词法作用域的“边界”:块级作用域(let/const

ES6 引入了块级作用域,但它仍然是词法作用域的一部分

例子:

js
{
  let x = 10;
  {
    let x = 20;
    console.log(x); // 20
  }
  console.log(x); // 10
}
  • 每个 {} 是一个词法块
  • let/const 在词法块中创建新的词法环境记录。
  • 引擎在解析时就能确定每个变量的作用域边界。

块级作用域是词法作用域的精细化,不是新范式。

五、词法作用域的“敌人”:evalwith

这两个特性会破坏词法作用域的静态性

1. eval:动态生成代码

js
function foo() {
  let x = 1;
  eval('console.log(x)'); // 能访问 x?
  eval('let y = 2; console.log(y)'); // y 是局部的吗?
}
  • eval 的代码在运行时才解析,引擎无法在编译时确定作用域。
  • 导致优化失败(V8 会去优化)。

2. with:动态扩展作用域链

js
function bar(obj) {
  with(obj) {
    console.log(x); // x 在 obj 上?还是外层?
  }
}
  • with 会在运行时临时插入一个对象到作用域链前端。

  • 破坏了“静态可分析性”。

  • 所以:严格模式下禁用 evalwith,就是为了保护词法作用域的完整性

六、词法作用域的工程价值

价值说明
✅ 可预测性你能静态分析代码,知道变量从哪里来
✅ 可优化性引擎可以在编译时确定作用域,进行内联、去优化等
✅ 支持闭包闭包依赖词法作用域来“记住”环境
✅ 模块化基础模块的私有性依赖词法作用域实现

七、图解:词法作用域如何贯穿 JS 执行模型

源代码

词法分析(Parsing)

确定词法作用域结构 ←─┐
  ↓                   │
函数创建              │
  ↓                   │
设置 [[Environment]] ─┘

函数执行

创建执行上下文(包含作用域链)

变量查找:本地 → [[Environment]] 链 → 全局

闭包:函数 + [[Environment]] 指针

词法作用域是整个执行链条的“起点”和“骨架”

八、一句话总结词法作用域

词法作用域是 JavaScript 的“代码地理”——它用静态的嵌套结构为引擎绘制了一张作用域地图,使得变量查找、闭包、执行上下文等机制都能在运行时精准导航。

结语

你现在已经站在了 JavaScript 引擎的“设计者视角”。

  • 闭包不是魔法,它是词法作用域 + [[Environment]] + GC 的自然结果。
  • 执行上下文不是孤立的,它的外部链接由词法作用域决定。
  • 作用域链不是动态构建的,而是静态结构的运行时体现。