深入 JS 执行机制:从 V8 引擎编译到作用域链的终极指南

4 阅读5分钟

在日常开发中,我们经常会遇到诸如 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:块级作用域,声明必须赋值,不可修改引用地址。

理解了这些底层逻辑,相信你在面对面试官的“夺命连环问”时,定能游刃有余!

 

 

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!我们下期见~