你真的了解 `var a = 2;` 的执行顺序吗?

133 阅读5分钟

很多人学 JavaScript 的时候,写下 var a = 2; 的时候,心里只是想着“定义一个变量并赋值”,然后就继续写下去,完全没有意识到这一行代码背后,引擎、编译器和作用域三者正进行一场精密的协作。

今天我们就来拆解这段“看似简单,实则奥妙无穷”的一行代码,从它背后的“编译三部曲”谈起,再深入理解作用域、查找机制、异常处理,最后通过拟人化对话加深理解。看完后,保准你再也不会小看 var a = 2;


一、作用域:变量的“居住证”系统

我们可以把作用域想象成变量的“房产登记处”。一旦你声明了一个变量,它就住进了某个作用域里。作用域负责:

  1. 安排谁住哪(变量归属);
  2. 查身份证(变量查找);
  3. 严格管理社区秩序(访问权限规则);

所以作用域的存在,是为了让程序不会变成“变量满天飞”的灾难现场。


二、JavaScript 的“即时编译”三部曲

虽然 JavaScript 是解释型语言,但它并不是边写边跑,而是在执行前先进行一轮“快速编译”,这个过程一般包括:

1. 分词(Tokenizing)

var a = 2;

这行代码会被拆解成多个词法单元(tokens):vara=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) → 对 fooRHS 查询(我要找到它并调用它);
  • a = 2 → 对 aLHS 查询(我要给你一个值);
  • console.log(a) → 对 aRHS 查询(我要用你的值);
  • consoleRHS 查询log 也是(从对象中拿属性);

为什么区分 LHS/RHS 很重要?

看下面这段代码:

function foo(a) {
  console.log(a + b);
  b = a;
}
foo(2);
  • 首先对 b 做了 RHS 查询:结果没找到 → 直接 ReferenceError。
  • 然后执行 b = aLHS 查询:非严格模式下,如果找不到,会直接在全局作用域创建一个新变量(太危险了)。

这就是为什么在严格模式下,如果你写 b = 123; 而没有 var/let/const 声明,JavaScript 会把你当成“非法移民”直接拒之门外。


五、作用域链:从里往外找变量

作用域是可以嵌套的,像一层层的洋葱。查找变量时:

  1. 先在当前作用域查;
  2. 找不到?向外层查;
  3. 一直找到全局作用域为止。

如果还是找不到,就爆炸(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,你有吧?

🧠 作用域:当然,afoo 的参数,昨天刚登记的。

👨‍💻 引擎:好勒,我把 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();

你觉得会输出什么?undefinedReferenceError?欢迎留言一起讨论👇


如果你觉得这篇文章对你有启发,不妨点个赞、转发一下~