在日常开发中,我们经常会遇到诸如 ReferenceError、变量值不符合预期等“灵异事件”。其实,要想真正玩转 JavaScript,光会写 API 是不够的,我们必须扒开 JS 引擎的外衣,看看它是如何编译和执行代码的。
今天,我们就从 V8 引擎的底层机制出发,彻底搞懂 JS 的编译过程、作用域以及 let/const/var 的本质区别。
一、V8 引擎是如何执行代码的?
很多同学以为,JS 是一门解释型语言,读取一行执行一行。大错特错! 无论是浏览器还是 Node.js,背后的霸主都是 V8 引擎,而 V8 的执行过程其实包含了编译阶段。
V8 引擎拿到你的代码后,并不会第一时间执行,而是先进行编译(梳理)。整个过程分为三步:
1. 分词:V8 会将源代码拆解成一个一个的词法单元。比如 const a = 1; 会被拆分成 const、a、=、1、;。
2. 解析/语法分析:V8 将这些词法单元按照语法规则,组织成一棵树状结构,也就是我们常说的 AST(抽象语法树,Abstract Syntax Tree)。这一步是 V8 理解你代码逻辑的关键。
3. 执行:AST 生成后,V8 会将其转化为机器码并开始执行。在执行的过程中,还会伴随着诸如 JIT(即时编译)等优化操作。
划重点:因为有了先编译后执行的机制,才衍生出了我们下面要讲的“声明提升”等特性。
二、JS 的三大作用域
作用域,简单来说就是变量的可见范围。它决定了代码中变量和其他资源的访问权限。在 JS 中,作用域分为三种:
1. 全局作用域
在代码的任何地方都能访问到的变量拥有全局作用域。比如最外层函数外定义的变量,或者未声明直接赋值的变量(非严格模式下)。
2. 函数作用域
JS 是函数级作用域语言。在函数内部定义的变量,只能在函数内部访问,外部无法读取。这保护了函数内部的变量不被污染。
3. 块级作用域(ES6)
在 ES6 之前,JS 没有“块”的概念(如 if、for 内部的 {})。ES6 的到来打破了这一限制,让 JS 终于拥有了块级作用域。
三、作用域查找规则:逐层向上,单向透明
理解了作用域的类型,我们来看看 V8 在执行代码时是如何查找变量的。
核心规则只有一条:由内向外,逐层查找。
1. 当 V8 遇到一个变量时,会先在当前作用域查找。
2. 如果找不到,就会去外层作用域查找。
3. 一直向外层层穿透,直到找到全局作用域。
4. 如果在全局作用域依然找不到,就会抛出 ReferenceError 错误。
特别注意:作用域是单向的!
外层作用域绝对不能访问内层作用域的变量。这就像你可以从房间看到外面的大街,但大街上的人看不到你房间里的东西一样。
function outer() {
let outerVar = '我是外层变量';
function inner() {
let innerVar = '我是内层变量';
console.log(outerVar); // ✅ 可以访问,逐层向外找到
}
inner();
console.log(innerVar); // ❌ 报错:ReferenceError,外层不能访问内层
}
outer();
四、let、const、var 的恩怨情仇
聊到作用域,就绕不开 let、const 和 var 这三剑客。它们不仅是声明变量的关键字,更是作用域规则的直接体现。
1. let + {} = 块级作用域
ES6 之前,var 声明的变量会无视 if 和 for 的 {},发生变量泄露(经典闭包循环问题)。而 let 只要遇到 {},就会形成一个新的块级作用域,变量被牢牢锁死在块内。
for (let i = 0; i < 3; i++) { // let 在这里形成了块级作用域,每次循环 i 都是独立的
}
console.log(i); // ❌ ReferenceError
for (var j = 0; j < 3; j++) {
// var 泄露到了全局
}
console.log(j); // ✅ 3
2. 声明提升:var 的原罪与 let 的 TDZ
回到我们开头讲的 V8 编译机制。V8 在编译阶段会先扫描代码,var 声明的变量会在编译阶段被提升到作用域的顶部,并赋值为 undefined,这就是声明提升。
而 let 和 const 不会带来声明提升。它们存在一个 TDZ(暂时性死区):从代码块开始,直到 let/const 声明那一行之前,变量都是不可访问的,强行访问会报错。
console.log(a); // undefined (var 声明提升,只提升声明不提升赋值)
var a = 1;
console.log(b); // ❌ ReferenceError (let 不提升,存在 TDZ)
let b = 2;
3. const:不可逾越的常量
const 的规则在 let 的基础上加了一条:声明时必须赋值,且赋值后不能再重新赋值。
避坑指南:const 不能重新赋值,但如果是引用数据类型(如对象、数组),其内部的属性是可以修改的!因为 const 保证的是变量指向的内存地址不变。
const obj = { name: '猪猪侠' };
obj.name = '猪猪侠'; // ✅ 修改属性可以,没有改变内存地址
obj = {}; // ❌ 报错:Assignment to constant variable,重新赋值改变了地址
五、总结
V8 执行机制:先编译(分词 -> AST),后执行。这就解释了为什么会有变量提升。
作用域类型:全局作用域、函数作用域、块级作用域(let + {})。
查找规则:由内向外逐层查找,外部无法访问内部。
变量声明:
var:函数作用域,存在声明提升。
let:块级作用域,存在 TDZ,不提升。
const:块级作用域,声明必须赋值,不可修改引用地址。
理解了这些底层逻辑,相信你在面对面试官的“夺命连环问”时,定能游刃有余!
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!我们下期见~