从变量提升到 V8 预编译,彻底搞懂 JS 执行机制

引言

  写 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)

步骤如下:

  1. 创建一个执行上下文对象 AO:{},初始值为空对象。
  2. 去函数体内找形参和变量声明,将形参和变量名作为属性名,添加到 AO 中,值为undfined。
  • 形参:a、b → AO添加属性a: undefined,b: undefined
  • 变量声明:var c :undefined
  • 此时AO:{a: undefined, b: undefined,c:undfined}
  1. 将形参和实参统一。
  • 函数调用时实参是1,对应形参a → a = 1
  • 实参只有一个,形参b没有对应实参,仍为undefined
  • 此时AO:{a: 1, b: undefined,c:undfined}
  1. 在函数体内找到函数声明,将函数名作为AO 中的属性名,函数体作为属性值。
  • 函数声明:function b() {} → AO添加属性b: function b() {}
  • 此时AO:{a: 1, b: function b() {}, c:undfined }

  最后输出结果为:

  如果还是觉得抽象,我给你打个最通俗的比方:我们把 V8 引擎比作一家公司的大老板,他脾气很倔,只认最终的执行指令,拿到手就直接干,绝不帮你整理乱七八糟的材料。而预编译,就是老板身边那个能力超强的全能女秘书。

  当你把一整段 JS 代码(也就是一堆待处理的工作任务)扔给公司时,绝不会直接堆到老板桌上。秘书会第一时间把所有任务全部过一遍,雷打不动做两件核心工作:

  1. 先把所有立项申请单独挑出来:也就是所有的var变量声明和function函数声明,提前拿去给老板签字审批。老板签完字,就代表这个项目(变量 / 函数)在公司系统里正式存在了,只是还没分配资源开始干活(默认值为undefined)。
  2. 再把剩下的具体执行任务按原顺序整理好:也就是赋值、运算、函数调用、打印这些真正干活的代码,等所有立项全部审批完成后,再按顺序交给老板执行。

 3.2 GO全局

  全局预编译,在代码加载完成后、执行之前触发,核心是创建GO对象(全局执行上下文对象),同样结合案例:

js 复制代码
function a () {}
console.log(a);
var a = 1
var b = 2

步骤如下

  1. 创建全局执行上下文 对象GO:{}
  2. 去全局体内找变量声明,将变量名作为属性名,添加到 GO 中,值为undfined。
  • 变量声明:var a,b → GO添加属性a: undefined,b: undefined;
  • 此时GO:{a: undefined,a: undefined}
  1. 将函数声明作为属性名,函数体作为属性值,添加到 GO 中。

-函数声明:function a() {}、 → GO属性中a重新被赋值: function a(){} -此时GO:{a: function a(){}, b: undefined}

最后输出:

4.总结

  到这里,我们就彻底搞懂了 JS 变量提升和预编译的底层逻辑。所有看似反直觉的变量提升现象,本质上都不是代码被物理移动到了作用域顶部,而是 V8 引擎在代码执行前,通过预编译阶段提前完成了所有声明的内存初始化。

  • 预编译的触发时机:全局预编译在代码加载完成后触发,函数预编译只在函数被调用的瞬间触发,函数执行完毕后对应的 AO 对象会立即销毁。
  • 预编译的核心工作:只处理var变量声明和function函数声明,完全不处理赋值、运算、打印等执行逻辑,这也是为什么变量提升后默认值是undefined的根本原因。
  • 两个核心对象的执行规则:
  1. 全局预编译(GO):创建全局执行上下文 → 收集所有var变量声明并赋值为undefined → 收集所有函数声明并赋值为函数体
  2. 函数预编译(AO):创建函数执行上下文 → 收集形参和var变量声明并赋值为undefined → 形参实参统一 → 收集内部函数声明并赋值为函数体
  • 黄金优先级规则:函数声明提升优先级 > 变量声明提升优先级,同名情况下函数声明会先占据内存,变量声明不会覆盖已有的函数声明,只有后续的赋值操作才会覆盖。
相关推荐
光影少年6 小时前
如果 React 组件的属性没有传值,它的默认值是什么?
前端·javascript·react.js
Helix2507 小时前
2026编程入门指南:零基础如何科学选择第一门编程语言?
java·c语言·javascript·零基础·编程语言·python入门·编程就业
鬼手点金8 小时前
opencode-隐私优先配置
服务器·前端·javascript·bug·openclaw
且从容.8 小时前
JS篇-----
java·javascript·jvm
吠品8 小时前
箭头函数为什么不能 new
开发语言·javascript·原型模式
苹果二8 小时前
使用Vue的指南
前端·javascript·vue.js
害人终害己9 小时前
‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
前端·javascript·vue.js
lerhxx1 天前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
mantou1321 天前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端
默_笙1 天前
🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
前端·javascript