很多人学 JavaScript 的时候,写下 var a = 2; 的时候,心里只是想着“定义一个变量并赋值”,然后就继续写下去,完全没有意识到这一行代码背后,引擎、编译器和作用域三者正进行一场精密的协作。
今天我们就来拆解这段“看似简单,实则奥妙无穷”的一行代码,从它背后的“编译三部曲”谈起,再深入理解作用域、查找机制、异常处理,最后通过拟人化对话加深理解。看完后,保准你再也不会小看 var a = 2;。
一、作用域:变量的“居住证”系统
我们可以把作用域想象成变量的“房产登记处”。一旦你声明了一个变量,它就住进了某个作用域里。作用域负责:
- 安排谁住哪(变量归属);
- 查身份证(变量查找);
- 严格管理社区秩序(访问权限规则);
所以作用域的存在,是为了让程序不会变成“变量满天飞”的灾难现场。
二、JavaScript 的“即时编译”三部曲
虽然 JavaScript 是解释型语言,但它并不是边写边跑,而是在执行前先进行一轮“快速编译”,这个过程一般包括:
1. 分词(Tokenizing)
var a = 2;
这行代码会被拆解成多个词法单元(tokens):var、a、=、2、;。这些 token 就像拼图的碎片,是语言引擎理解语义的第一步。
有趣的是,空格是否是 token,取决于语言设计。JavaScript 里空格基本是“空气”,在 Python 里却是“地基”。
2. 解析(Parsing)
解析阶段会把这些 token 组装成一棵语法树(AST,抽象语法树)。你可以理解为 JavaScript 程序的“骨架图”,编译器靠它来理解程序结构。
3. 代码生成(Code Generation)
最终,编译器会根据语法树生成可执行代码,比如:
- 告诉作用域“我要声明一个变量叫 a”;
- 给引擎编写执行脚本:“将 2 放入 a 中”。
这套流程类似一个“城市建设”:分词是拆包,解析是设计图,生成是动工施工。
三、深入 var a = 2;:编译器 & 引擎的分工合作
编译器阶段:
var a;
-
编译器首先检查作用域:你这儿有叫
a的变量吗?- 有?那我就不管了。
- 没有?那我现在声明一个叫
a的变量!
执行阶段(引擎):
a = 2;
-
引擎:作用域兄,我要把 2 放进
a,你这有a吗?- 有?那我就放心大胆地赋值了。
- 没有?那我继续往上找,找不到我可就炸了(ReferenceError)!
四、LHS vs RHS:别再混淆左右手
JavaScript 里查找变量,有两种方式:
| 类型 | 含义 |
|---|---|
| LHS(Left-Hand Side) | 找“变量的容器” → 谁接收值? |
| RHS(Right-Hand Side) | 找“变量的值” → 要拿值用! |
比如这段代码:
function foo(a) {
console.log(a);
}
foo(2);
我们来标记一下查找过程:
foo(2)→ 对foo是 RHS 查询(我要找到它并调用它);a = 2→ 对a是 LHS 查询(我要给你一个值);console.log(a)→ 对a是 RHS 查询(我要用你的值);console是 RHS 查询,log也是(从对象中拿属性);
为什么区分 LHS/RHS 很重要?
看下面这段代码:
function foo(a) {
console.log(a + b);
b = a;
}
foo(2);
- 首先对
b做了 RHS 查询:结果没找到 → 直接 ReferenceError。 - 然后执行
b = a是 LHS 查询:非严格模式下,如果找不到,会直接在全局作用域创建一个新变量(太危险了)。
这就是为什么在严格模式下,如果你写 b = 123; 而没有 var/let/const 声明,JavaScript 会把你当成“非法移民”直接拒之门外。
五、作用域链:从里往外找变量
作用域是可以嵌套的,像一层层的洋葱。查找变量时:
- 先在当前作用域查;
- 找不到?向外层查;
- 一直找到全局作用域为止。
如果还是找不到,就爆炸(ReferenceError)。
function outer() {
let a = 1;
function inner() {
console.log(a); // 找不到就去 outer 作用域找
}
inner();
}
outer();
六、模拟一次引擎和作用域的对话(轻松理解)
来看个拟人化的对话:
function foo(a) {
console.log(a);
}
foo(2);
👨💻 引擎:喂作用域,我要找 foo,RHS 查询,快点!
🧠 作用域:这你算找对人了,刚声明不久,它是个函数。拿去用。
👨💻 引擎:Nice,我执行 foo 了,现在我得 LHS 查询 a,你有吧?
🧠 作用域:当然,a 是 foo 的参数,昨天刚登记的。
👨💻 引擎:好勒,我把 2 塞进 a。
👨💻 引擎:console 是啥?RHS 查询!
🧠 作用域:这个是内置大佬,给你用。
👨💻 引擎:完美!我找找 log…找到了,是函数!
👨💻 引擎:a 的值是多少来着?RHS 查一下…
🧠 作用域:还是 2,稳得很!
👨💻 引擎:执行 log(2),圆满收官!
七、总结:var a = 2 背后是门哲学
变量声明与赋值,从来都不是一个动作完成的,它们分别发生在不同阶段,由不同角色负责:
- 编译器 负责声明(
var a); - 引擎 负责赋值(
a = 2); - 作用域 负责管理访问权限(有没有权访问变量);
理解了编译阶段、LHS/RHS 查找机制和作用域链后,我们就能更自信地回答面试官的问题,也能避免很多“莫名其妙”的运行时错误。
彩蛋思考题
function test() {
console.log(a); // 输出什么?
var a = 10;
}
test();
你觉得会输出什么?undefined?ReferenceError?欢迎留言一起讨论👇
如果你觉得这篇文章对你有启发,不妨点个赞、转发一下~