引言
写 JS 谁没被变量提升坑过?明明变量写在后面,前面却打印出 undefined;函数定义在末尾,开头就能直接调用。很多人背了 "声明会提升" 的口诀,却以为是代码被偷偷搬到了顶部 ------ 大错特错!这根本不是什么玄学bug,而是V8引擎在代码执行前悄悄搞的预编译小动作。
1.变量提升现象
那什么是变量提升呢?接下来用一段代码告诉你:
js
console.log(a);
var a=2;
输出结果为:

你看,我们明明先打印 a、后定义 a,输出的却不是预期的报错,而是undefined。这就是前端入门必踩的"变量声明提升"坑。它的本质是:V8 引擎在代码执行前,会先偷偷把变量 a 的声明提到当前作用域的最前面,赋值操作却原封不动留在原地。所以这段代码的实际执行顺序,相当于引擎帮你偷偷改成了这样:

2.什么是V8引擎的预编译?
- 预编译发生在代码执行之前,但并不是"一次性编译完所有代码",函数的预编译是在"函数被调用时"才触发的,全局预编译在代码加载时触发。
- 预编译只处理"声明"(变量声明var、函数声明function),不处理"赋值操作"。这也是为什么变量提升后,值是undefined的原因。
- let、const没有变量提升,严格来说是"暂时性死区",本质是V8引擎对let、const的预编译处理和var不同,后续会在误区中补充。
3.预编译的两个场景 GO 全局/AO 函数
3.1 AO函数
函数的预编译,只有在函数被调用时才会触发,核心是创建AO对象(执行上下文对象),用一段代码解释:
js
function foo(a, b) {
console.log(b);
c = 0
var c
a = 3
b = 2
console.log(b);
function b() {}
console.log(b);
}
foo(1)
步骤如下:
- 创建一个执行上下文对象 AO:{},初始值为空对象。
- 去函数体内找形参和变量声明,将形参和变量名作为属性名,添加到 AO 中,值为undfined。
- 形参:a、b → AO添加属性a: undefined,b: undefined
- 变量声明:var c :undefined
- 此时AO:{a: undefined, b: undefined,c:undfined}
- 将形参和实参统一。
- 函数调用时实参是1,对应形参a → a = 1
- 实参只有一个,形参b没有对应实参,仍为undefined
- 此时AO:{a: 1, b: undefined,c:undfined}
- 在函数体内找到函数声明,将函数名作为AO 中的属性名,函数体作为属性值。
- 函数声明:function b() {} → AO添加属性b: function b() {}
- 此时AO:{a: 1, b: function b() {}, c:undfined }
最后输出结果为:

如果还是觉得抽象,我给你打个最通俗的比方:我们把 V8 引擎比作一家公司的大老板,他脾气很倔,只认最终的执行指令,拿到手就直接干,绝不帮你整理乱七八糟的材料。而预编译,就是老板身边那个能力超强的全能女秘书。
当你把一整段 JS 代码(也就是一堆待处理的工作任务)扔给公司时,绝不会直接堆到老板桌上。秘书会第一时间把所有任务全部过一遍,雷打不动做两件核心工作:
- 先把所有立项申请单独挑出来:也就是所有的var变量声明和function函数声明,提前拿去给老板签字审批。老板签完字,就代表这个项目(变量 / 函数)在公司系统里正式存在了,只是还没分配资源开始干活(默认值为undefined)。
- 再把剩下的具体执行任务按原顺序整理好:也就是赋值、运算、函数调用、打印这些真正干活的代码,等所有立项全部审批完成后,再按顺序交给老板执行。
3.2 GO全局
全局预编译,在代码加载完成后、执行之前触发,核心是创建GO对象(全局执行上下文对象),同样结合案例:
js
function a () {}
console.log(a);
var a = 1
var b = 2
步骤如下
- 创建全局执行上下文 对象GO:{}
- 去全局体内找变量声明,将变量名作为属性名,添加到 GO 中,值为undfined。
- 变量声明:var a,b → GO添加属性a: undefined,b: undefined;
- 此时GO:{a: undefined,a: undefined}
- 将函数声明作为属性名,函数体作为属性值,添加到 GO 中。
-函数声明:function a() {}、 → GO属性中a重新被赋值: function a(){} -此时GO:{a: function a(){}, b: undefined}
最后输出:

4.总结
到这里,我们就彻底搞懂了 JS 变量提升和预编译的底层逻辑。所有看似反直觉的变量提升现象,本质上都不是代码被物理移动到了作用域顶部,而是 V8 引擎在代码执行前,通过预编译阶段提前完成了所有声明的内存初始化。
- 预编译的触发时机:全局预编译在代码加载完成后触发,函数预编译只在函数被调用的瞬间触发,函数执行完毕后对应的 AO 对象会立即销毁。
- 预编译的核心工作:只处理var变量声明和function函数声明,完全不处理赋值、运算、打印等执行逻辑,这也是为什么变量提升后默认值是undefined的根本原因。
- 两个核心对象的执行规则:
- 全局预编译(GO):创建全局执行上下文 → 收集所有var变量声明并赋值为undefined → 收集所有函数声明并赋值为函数体
- 函数预编译(AO):创建函数执行上下文 → 收集形参和var变量声明并赋值为undefined → 形参实参统一 → 收集内部函数声明并赋值为函数体
- 黄金优先级规则:函数声明提升优先级 > 变量声明提升优先级,同名情况下函数声明会先占据内存,变量声明不会覆盖已有的函数声明,只有后续的赋值操作才会覆盖。