前言:为什么你写的代码“不按常理出牌”?
你是否遇到过这样的困惑?
console.log(a); // undefined
var a = 2;
console.log(b); // ReferenceError: b is not defined
为什么 var a 可以“提前”访问,而 b 却直接报错?
为什么函数能“记住”外部变量,即使外层函数已经执行完毕?
这些问题的答案,都藏在 JavaScript 的作用域机制中。
本文将带你从编译原理出发,层层深入,彻底搞懂 JavaScript 的作用域、作用域链、LHS/RHS 查询、以及闭包的本质。
一、JavaScript 也是“编译型”语言?
很多人认为 JavaScript 是“解释执行”的脚本语言,但实际上,它也经历了一个编译过程,只不过这个过程发生在代码执行前的几微秒内。
JavaScript 的编译三步曲
1. 分词 / 词法分析(Tokenizing / Lexing)
将代码字符串分解为有意义的“词法单元(tokens)”。
var a = 2;
会被分解为:
var(关键字)a(标识符)=(赋值操作符)2(数值字面量);(语句结束符)
空格是否重要?取决于语言规则。JS 中空格通常可忽略,但像
a++不能写成a ++(虽然语法允许,但可读性差)。
2. 解析 / 语法分析(Parsing)
将词法单元转换为抽象语法树(AST)。
例如:
var a = 2;
AST 可能是:
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "a" },
"init": { "type": "Literal", "value": 2 }
}
],
"kind": "var"
}
AST 是后续代码生成的基础。
3. 代码生成
将 AST 转换为可执行的机器指令或字节码。
⚡ 注意:JS 的编译发生在执行前的瞬间,所以你感觉不到“先编译再运行”的过程。
二、作用域的本质:变量的“查找规则”
作用域,简单说就是变量和函数的可访问范围。
但更准确的定义是:
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
作用域的两种类型
| 类型 | 特点 |
|---|---|
| 全局作用域 | 在整个程序中都可访问 |
| 局部作用域 | 只在函数或块级作用域内可访问 |
var globalVar = '全局变量';
function foo() {
var localVar = '局部变量';
console.log(globalVar); // ✅ 可访问
console.log(localVar); // ✅ 可访问
}
foo();
console.log(localVar); // ❌ 报错!localVar is not defined
三、作用域链:变量的“寻亲之旅”
当 JavaScript 引擎查找一个变量时,它不会只在当前作用域中找,而是一层层向上查找,直到全局作用域。
这就是 作用域链(Scope Chain)。
示例:
var a = 1;
function outer() {
var b = 2;
function inner() {
var c = 3;
console.log(a + b + c); // 6
}
inner();
}
outer();
inner 函数中访问 a 和 b 的过程:
- 在
inner作用域中找a→ 找不到 - 向上到
outer作用域找a→ 找不到 - 向上到全局作用域找
a→ 找到,值为 1 - 同理找到
b = 2
作用域链是静态的,在函数定义时就确定了,与调用位置无关(词法作用域)。
四、LHS 与 RHS:变量查询的两种方式
JavaScript 引擎在查找变量时,会区分两种查询:
| 查询类型 | 含义 | 示例 |
|---|---|---|
| LHS | “赋值操作的目标是谁?” | a = 2 中的 a |
| RHS | “谁是赋值操作的源头?” | console.log(a) 中的 a |
重要区别
1. RHS 查询失败 → ReferenceError
console.log(a); // RHS 查询 a
// 如果找不到 a,抛出 ReferenceError
2. LHS 查询失败 → 在全局创建变量(非严格模式)
function foo() {
b = 2; // LHS 查询 b
}
foo();
console.log(b); // 2!b 被自动创建为全局变量
⚠️ 在 严格模式(
'use strict')下,LHS 查询失败也会抛出ReferenceError,避免意外创建全局变量。
五、闭包:函数的“记忆能力”
闭包是 JavaScript 中最强大也最令人困惑的概念之一。
定义:
闭包是指一个函数能够访问其外层作用域中的变量,即使外层函数已经执行完毕。
经典示例:
function outer() {
var count = 0;
return function inner() {
count++;
console.log(count);
};
}
var counter = outer(); // outer 执行完毕,但 count 未被销毁
counter(); // 1
counter(); // 2
counter(); // 3
为什么 count 没有被销毁?
inner函数引用了outer作用域中的count- 只要
inner存在,count就不会被垃圾回收 - 这就是闭包的“记忆”能力
闭包常用于:模块化、私有变量、函数柯里化、防抖节流等。
六、作用域的实际应用与陷阱
1. 避免全局变量污染
// ❌ 错误:大量全局变量
var a = 1;
var b = 2;
var c = 3;
// ✅ 正确:使用立即执行函数(IIFE)创建私有作用域
(function() {
var a = 1;
var b = 2;
// 外部无法访问 a、b
})();
2. 循环中的闭包问题
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 3 3 3
}, 100);
}
问题:var 没有块级作用域,i 是全局的。
修复方案 1:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 0 1 2
}, 100);
}
修复方案 2:使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 输出 0 1 2
}, 100);
})(i);
}
总结:一张表看懂核心概念
| 概念 | 说明 |
|---|---|
| 编译过程 | 分词 → 解析(AST)→ 代码生成 |
| 作用域 | 变量的查找规则 |
| 作用域链 | 从内到外逐层查找变量 |
| LHS/RHS | LHS 找“赋值目标”,RHS 找“赋值源头” |
| 闭包 | 函数“记住”外层变量的能力 |
结语:掌握作用域,才能真正理解 JavaScript
作用域是 JavaScript 的基石概念。
理解它,你才能明白:
- 为什么变量能“提前访问”
- 为什么函数能“记住”外部变量
- 如何避免常见的
ReferenceError - 如何写出更安全、更模块化的代码
建议:动手写几个闭包例子,调试查看作用域链,加深理解。