你不知道的 JavaScript(上卷)第一章:作用域是什么
本文整理自《你不知道的 JavaScript(上卷)》第一、二章,旨在帮助读者从编译器视角理解 JavaScript 的作用域机制与词法作用域。
一、谁在负责查找变量?
在传统印象中,JavaScript 引擎似乎包揽了所有任务:执行代码、管理变量、处理内存。
但实际上,还有两个同样重要的参与者:
- JS 引擎:负责编译并执行代码。
- 编译器(Compiler) :负责语法分析与代码生成。
- 作用域(Scope) :收集并维护所有声明的标识符,并制定变量访问规则。
关键点:JavaScript 并非完全“解释执行”,而是有一个即时编译(JIT)过程。
二、JavaScript 的编译过程(以 var a = 2 为例)
1. 分词 / 词法分析(Tokenizing / Lexing)
将代码分解为有意义的最小单元(token):
var | a | = | 2 | ;
2. 解析 / 语法分析(Parsing)
将 tokens 转换为抽象语法树(AST)。
3. 代码生成(Code Generation)
将 AST 转换为可执行的机器指令,引擎根据这些指令创建变量并执行赋值操作。
三、LHS 与 RHS 查询
LHS(Left-hand Side)查询
- 出现在赋值操作左侧,用于查找赋值目标。
- 目标是找到变量的“容器”位置。
a = 2; // 找到 a 的存储位置 —— LHS 查询
RHS(Right-hand Side)查询
- 出现在赋值右侧,用于获取变量的值。
console.log(a); // 取 a 的值 —— RHS 查询
理解比喻:
- LHS:找一个“书架”放书。
- RHS:找一本具体的“书”。
四、作用域嵌套与查找规则
JavaScript 的作用域是分层嵌套的结构,形成所谓的“作用域链”(Scope Chain)。
查找变量时,遵循以下规则:
- 从当前作用域开始查找;
- 若未找到,逐层向外查找;
- 若查找到全局作用域仍未找到,则抛出错误。
具体情况:
-
LHS 查询:
- 非严格模式:自动创建全局变量;
- 严格模式:抛出
ReferenceError。
-
RHS 查询:
无论是否严格模式,均抛出ReferenceError。
五、常见错误类型
| 错误类型 | 触发条件 |
|---|---|
ReferenceError | RHS 查询失败(变量未定义) |
TypeError | 变量存在,但操作非法(如对 undefined 调用函数) |
六、第二章:词法作用域(Lexical Scope)
JavaScript 的作用域模型主要有两种:
- 词法作用域(Lexical Scope)
- 动态作用域(Dynamic Scope)
JavaScript 实际采用的是 词法作用域。
1. 什么是词法作用域?
词法作用域指:作用域由代码书写时的位置决定,在编译阶段就已确定。
与之相对的动态作用域,是在运行时根据调用位置决定的。
除了 eval 和 with,JavaScript 作用域基本都是词法作用域。
2. 词法作用域的两条规则
规则一:定义时决定作用域
函数或块的作用域环境在声明时就已经确定,不会被运行时改变。
规则二:运行时无法修改词法作用域
eval(str)可以在运行时生成变量;with(obj)可以临时扩展作用域链。
但这两者都会破坏作用域的静态性与性能。
建议:避免使用 eval 和 with。
3. “欺骗”词法作用域的代价
eval(str)
function foo(str) {
eval(str);
console.log(b);
}
foo("var b = 3;"); // 输出 3
问题:引擎无法在编译阶段优化作用域,运行时性能下降。
with(obj)
with (obj) {
a = 2; // 可能修改 obj.a,也可能修改全局 a
}
问题:变量归属模糊,调试困难,已被废弃。
4. 作用域气泡模型(Scope Bubbles)
每个作用域可视为一个“气泡”:
- 全局作用域是最外层气泡;
- 每个函数创建新的气泡;
- ES6 的
let和const会创建块级作用域。
function foo() {
var a = 1;
if (true) {
let b = 2; // 块级作用域
}
console.log(a); // 1
console.log(b); // ReferenceError
}
5. 作用域链与性能优化
作用域链越深,变量查找速度越慢(差距虽小,但确实存在)。
优化建议:
- 减少全局变量;
- 将常用变量放在当前作用域;
- 使用 IIFE(立即执行函数表达式)创建私有作用域。
七、第一章与第二章的关联
| 概念 | 说明 |
|---|---|
| 编译三步走 | 分词 → 解析 → 代码生成 |
| LHS vs RHS | 赋值目标 vs 获取值 |
| 作用域链查找 | 从内到外逐层查找 |
| 词法作用域 | 写代码时决定作用域 |
| 欺骗词法作用域 | eval / with 会破坏可预测性 |
八、核心总结
- JavaScript 在执行前会经历编译阶段。
- 作用域是一套规则,决定变量的归属与访问权限。
- 理解 LHS 和 RHS 是掌握作用域机制的关键。
- 词法作用域在代码书写阶段就已确定。
- 避免使用
eval和with,它们会影响性能和可维护性。
九、学习建议
- 多写代码验证 LHS / RHS 的行为。
- 使用浏览器开发者工具查看作用域链。
- 尝试从“编译器视角”理解变量声明与查找。
- 通过绘制函数嵌套结构理解词法作用域的静态特性。