再探词法作用域
目标:理解词法作用域不仅是“静态的”,更是 JavaScript 引擎构建作用域链、执行上下文、闭包的底层依据。
一、词法作用域的本质:静态的代码结构
什么是词法作用域?
词法作用域(Lexical Scope) 是指变量和函数的作用域由它们在源代码中的位置决定,而不是在运行时动态决定。
例子:
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 不是这样):
let x = "global";
function foo() {
console.log(x);
}
function bar() {
let x = "bar";
foo(); // 如果是动态作用域,应输出 "bar"
}
bar(); // 实际输出 "global" ← 因为 foo 定义在全局,只能访问全局 xJS 输出 "global",说明它是词法作用域:foo 的作用域在它定义时就固定了。
三、词法作用域如何影响执行?——从代码到执行上下文
词法作用域不是“死的”,它在运行时通过以下机制驱动执行:
1. 构建作用域链(Scope Chain)
- 作用域链是一条由
[[Environment]]指针连接的词法环境链。 - 每个函数都有
[[Environment]],指向它定义时的词法环境。 - 变量查找时,沿着这条链向上找。
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]]。 - 这个过程是静态的,发生在解析阶段,而不是执行阶段。
function makeFn() {
let env = "I'm here";
return function() {
console.log(env);
};
}匿名函数在
makeFn内部定义 → 它的[[Environment]]指向makeFn的词法环境。这个关系在函数创建时就定死了,无论它后来在哪里调用。
词法作用域是
[[Environment]]的“设计图”。
3. 影响执行上下文的创建
当函数执行时,会创建执行上下文,其中:
LexicalEnvironment:当前函数的局部作用域。[[Environment]]:指向外层词法环境(由词法作用域决定)。
// 执行 inner 时:
{
LexicalEnvironment: { z: 3 }, // 本地变量
VariableEnvironment: { z: 3 },
ThisBinding: ...,
// 外部查找:
[[Environment]]: 指向 outer 的词法环境 → 可以访问 y
}所以:词法作用域决定了执行上下文的“外部链接”。
四、词法作用域的“边界”:块级作用域(let/const)
ES6 引入了块级作用域,但它仍然是词法作用域的一部分。
例子:
{
let x = 10;
{
let x = 20;
console.log(x); // 20
}
console.log(x); // 10
}- 每个
{}是一个词法块。 let/const在词法块中创建新的词法环境记录。- 引擎在解析时就能确定每个变量的作用域边界。
块级作用域是词法作用域的精细化,不是新范式。
五、词法作用域的“敌人”:eval 和 with
这两个特性会破坏词法作用域的静态性:
1. eval:动态生成代码
function foo() {
let x = 1;
eval('console.log(x)'); // 能访问 x?
eval('let y = 2; console.log(y)'); // y 是局部的吗?
}eval的代码在运行时才解析,引擎无法在编译时确定作用域。- 导致优化失败(V8 会去优化)。
2. with:动态扩展作用域链
function bar(obj) {
with(obj) {
console.log(x); // x 在 obj 上?还是外层?
}
}with会在运行时临时插入一个对象到作用域链前端。破坏了“静态可分析性”。
所以:严格模式下禁用
eval和with,就是为了保护词法作用域的完整性。
六、词法作用域的工程价值
| 价值 | 说明 |
|---|---|
| ✅ 可预测性 | 你能静态分析代码,知道变量从哪里来 |
| ✅ 可优化性 | 引擎可以在编译时确定作用域,进行内联、去优化等 |
| ✅ 支持闭包 | 闭包依赖词法作用域来“记住”环境 |
| ✅ 模块化基础 | 模块的私有性依赖词法作用域实现 |
七、图解:词法作用域如何贯穿 JS 执行模型
源代码
↓
词法分析(Parsing)
↓
确定词法作用域结构 ←─┐
↓ │
函数创建 │
↓ │
设置 [[Environment]] ─┘
↓
函数执行
↓
创建执行上下文(包含作用域链)
↓
变量查找:本地 → [[Environment]] 链 → 全局
↓
闭包:函数 + [[Environment]] 指针词法作用域是整个执行链条的“起点”和“骨架”。
八、一句话总结词法作用域
词法作用域是 JavaScript 的“代码地理”——它用静态的嵌套结构为引擎绘制了一张作用域地图,使得变量查找、闭包、执行上下文等机制都能在运行时精准导航。
结语
你现在已经站在了 JavaScript 引擎的“设计者视角”。
- 闭包不是魔法,它是词法作用域 +
[[Environment]]+ GC 的自然结果。 - 执行上下文不是孤立的,它的外部链接由词法作用域决定。
- 作用域链不是动态构建的,而是静态结构的运行时体现。