在 JavaScript 世界中,代码的执行并非简单的自上而下逐行解析,而是存在一个 "预编译" 阶段。这个阶段往往在我们肉眼不可见的情况下完成,但却深刻影响着代码的执行结果。今天我们就来深入探讨 JavaScript 的预编译机制,带你揭开它的神秘面纱。
什么是预编译?
当 V8 引擎读取到 JavaScript 代码时,并不是立即执行,而是先进行编译处理,然后再执行。这个编译过程我们通常称为 "预编译"。预编译的核心作用是:将代码中的声明部分提升到当前作用域的顶部。
这也就解释了为什么我们可以在变量声明前使用变量,在函数声明前调用函数。
全局作用域的预编译
当 JavaScript 引擎处理全局作用域时,会经历以下预编译步骤:
-
创建全局执行上下文(GO 对象)
全局执行上下文(Global Object)是一个特殊的对象,它会存储全局作用域中的变量和函数。
-
处理变量声明
查找所有变量声明(使用
var声明的变量),将变量名作为 GO 对象的属性,初始值设为undefined。 -
处理函数声明
查找所有函数声明,将函数名作为 GO 对象的属性,值为函数体本身。
让我们通过一个例子来理解:
js
var a = 1
function a() {}
console.log(a); // 1
var b = a
console.log(b); // 1
a = 2
var c = b
console.log(c); // 1
其预编译过程中创建的 GO 对象变化如下:
js
GO: {
a: undefined → function a() {} → 1 → 2,
b: undefined → 1,
c: undefined → 1
}
函数作用域的预编译
函数的预编译过程比全局作用域更复杂一些,具体步骤如下:
-
创建函数执行上下文(AO 对象)
当函数被调用时,会创建一个函数执行上下文(Activation Object),专门存储该函数作用域内的变量和函数。
-
处理形参和变量声明
查找函数的形参和变量声明(
var声明),将它们作为 AO 对象的属性,初始值设为undefined。 -
形参和实参统一
将传入的实参值赋值给对应的形参。
-
处理函数声明
在函数体内查找所有函数声明,将函数名作为 AO 对象的属性,值为函数体本身。
同样,我们通过一个例子来理解:
js
function fn(a) {
console.log(a); // function a() {}
var a = 123
console.log(a); // 123
function a() {}
var b = function() {}
console.log(b); // function() {}
function c() {}
var c = a
console.log(c); // 123
}
fn(1)
该函数预编译过程中 AO 对象的变化:
js
AO: {
a: undefined → 1 → function a() {} → 123,
b: undefined → function() {},
c: undefined → function c() {} → 123
}
预编译与执行的关系
预编译完成后,JavaScript 引擎才会开始执行代码。执行阶段会按照代码的书写顺序,逐行处理赋值操作和其他可执行语句,更新 AO/GO 对象中的属性值。
我们再看一个包含作用域链的例子:
js
// GO:{
// g: undefined 100,
// fn: function() {}
// }
g = 100
function fn() {
console.log(g); // undefined
g = 200
console.log(g); // 200
var g = 300
}
// AO: {
// g: undefined 200 300
// }
fn()
var g
在这个例子中:
-
全局预编译创建 GO 对象,包含
g: undefined和fn: 函数体 -
函数 fn 被调用时创建 AO 对象,包含
g: undefined -
执行时,函数内部首先访问的是 AO 中的
g(初始为 undefined),而不是全局的g
常见预编译陷阱
-
变量提升 vs 函数提升
函数声明会被完整提升(包括函数体),而变量声明只提升声明部分,赋值部分留在原地。
-
重复声明处理
对象中不能有重复的 key,后声明的会覆盖先声明的:
js
let obj = {
a: 1
}
obj.a = 2 // 覆盖原有值
-
函数参数与内部变量的优先级
函数参数会被初始化为 undefined,然后接收实参值,之后会被内部函数声明覆盖。
总结
JavaScript 预编译是理解代码执行机制的关键环节,掌握它能帮助我们:
-
理解变量提升和函数提升的本质
-
预测代码的执行结果
-
避免因作用域问题导致的 bugs
预编译的核心就是在代码执行前,先创建相应的执行上下文(GO/AO),并对声明部分进行处理。记住这个过程,你就能更深入地理解 JavaScript 的运行机制。