前言
在 JavaScript 的世界里,V8 引擎就像一位幕后导演,对代码进行 “编译 + 执行” 的两步走演绎。这其中,声明提升与执行上下文是核心剧情,让我们用色彩与细节来拆解这场精彩的表演。
请坐上学习的快车,前方高能来袭!
一、编译先行:V8 引擎的 “剧本预处理”
当 V8 读取 JS 代码时,会先进入编译阶段,把所有声明部分(变量声明、函数声明)悄悄 “提升” 到当前作用域的顶部。这就像话剧开演前,道具和演员先就位 —— 代码执行前,声明已经在 “幕后” 准备好了。
(一)函数执行上下文:AO 对象的诞生
当函数体被编译时,会创建函数的执行上下文,对应一个 “演员休息室”——AO(Activation Object,活动对象) 。它的诞生遵循以下步骤:
- 找形参和变量声明:把形参和
var声明的变量作为 AO 的属性,初始值为undefined。 - 实参形参统一:如果调用函数时传了实参,就把实参的值赋给对应的形参属性。
- 找函数声明:把函数声明的名称作为 AO 的属性,值为完整的函数体。
我们来看一个例子:
function fn(a) {
console.log(a); // 第一步:AO 中 a 先被赋值为函数体 function a() {}
var a = 123 // 第二步:a 被赋值为 123
console.log(a); // 输出123
function a() {} // 函数声明被提升到 AO,覆盖早期的形参定义
var b = function() {} // 函数表达式,b 现在 AO 中为 undefined,执行时才赋值函数体 function() {}
console.log(b); // 输出 function() {}
function c() {} // 函数声明被提升到 AO
var c = a // c 被赋值为 123
console.log(c); // 输出123
}
AO分析如下:
运行结果和我们预期的一致:
在 AO 中,变量和函数的 “就位顺序” 是:函数声明 > 变量声明,所以函数声明会覆盖同名变量的早期定义。
(二)全局执行上下文:GO 对象的统治
那么聊完 AO 对象,接下来该 GO 登场啦!当全局代码被编译时,会创建全局执行上下文,对应 “全局后台”——GO(Global Object,全局对象) 。它的诞生步骤是:
- 找变量声明(
var声明) :把变量名作为 GO 的属性,初始值为undefined。 - 找函数声明:把函数名作为 GO 的属性,值为完整的函数体。
依旧上代码:
var a = 1 // 变量声明被提升到 GO,初始为 undefined,执行时赋值 1
function a() {} // 函数声明被提升到 GO,覆盖变量 a 的早期 undefined
console.log(a); // 输出 1
var b = a // b 被赋值为 a 的当前值 1
console.log(b); // 输出 1
a = 2 // a 被重新赋值为 2
var c = b // c 被赋值为 b 的值 1
console.log(c); // 输出1
GO 分析如下:
运行结果也和分析的一致:
在 GO 中,函数声明的优先级高于变量声明,但如果变量有赋值操作,最终值会以赋值为准。
二、用 “老板与秘书” analogy 秒懂 V8 执行逻辑
在之前对编译阶段、AO/GO 对象的解析基础上,我们可以用 “老板与秘书” 的生活化 analogy,把 V8 引擎的 “编译 - 执行” 流程讲得更生动。
把 V8 引擎想象成一家公司,编译部门是 “秘书” ,执行部门是 “老板” ,而 JavaScript 代码就是 “待处理的工作任务”。
- 秘书(编译部门) :接到任务(代码)后,先做 “预处理” —— 把所有变量声明、函数声明提前整理好,记在 “工作备忘录”(AO/GO 对象)里,确保老板问起时能立刻回答。
- 老板(执行部门) :只负责按顺序处理具体业务(执行代码的赋值、运算、输出等操作),但遇到 “变量 / 函数” 时,会先问秘书 “这个东西之前记了吗?”(即从 AO/GO 中查找定义)。
就拿 AO 的来代码举例子:
function fn(a) {} // 函数声明
fn(1) // 函数调用
- 秘书(编译部门)预处理函数声明:当秘书看到
function fn(a){}时,立刻在 “全局备忘录(GO)” 里记下:fn是一个函数,内容是function fn(a) {}。 - 老板(执行部门)执行函数调用:当老板执行到
fn(1)时,先问秘书:“fn是什么?”秘书翻开备忘录(GO):“是个函数,这就给您调过来执行!”于是函数fn开始执行 —— 此时秘书又会为这个函数单独建一个 “部门备忘录(AO)” ,记录函数内的形参a、变量声明、函数声明等信息,供函数执行过程中查询。
上图更清晰!
再延伸到变量提升的场景:
console.log(a); // 老板问:a 是什么?
var a = 1; // 秘书提前记在备忘录:a 初始是 undefined,执行时赋值 1
- 秘书(编译)阶段:在 GO 里记上
a: undefined。 - 老板(执行)阶段:执行
console.log(a)时,先问秘书,得到undefine;执行a = 1时,让秘书把备忘录里的a改成1。
秘书的 “备忘录” 与老板的 “执行力”
- 秘书(编译) :负责 “声明提升”,把变量、函数的 “身份信息” 提前记在 AO/GO 这两个 “备忘录” 里,确保信息可查。
- 老板(执行) :负责 “按顺序做事”,遇到变量 / 函数就查秘书的备忘录,然后执行具体操作(赋值、调用等)。
通过这个 analogy,就能把抽象的编译机制转化为日常场景,轻松理解 V8 引擎 “先编译预处理,再执行代码” 的核心逻辑~
看到这里相信屏幕前的你已经拿捏了 ٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ 来几道例子try try吧
先自己分析再看运行结果哦~
三、核心结论:编译阶段的 “潜规则”
- 声明提升只提升 “名称” :
var声明的变量提升后值为undefined,函数声明提升后值为完整函数体。 - 作用域内优先级:函数声明 > 变量声明;执行阶段的赋值操作会覆盖声明阶段的定义。
- AO 与 GO 的边界:函数执行时创建 AO,全局代码执行时创建 GO,二者相互隔离又通过作用域链关联。
总结
✨ 核心收获:吃透编译机制,直抵 JS 执行本质 ✨
掌握 声明提升优先级(函数声明 > 变量声明)、AO/GO 上下文创建规则 这些编译阶段的 “底层潜规则”,就像手握 JS 执行逻辑的 “透视镜”—— 既能精准看透变量 / 函数的 “提前就位” 逻辑,又能清晰划分作用域边界,从此告别变量提升导致的 undefined 困惑、作用域混淆引发的取值异常等 “坑”,让代码执行流程了然于胸,疑难问题迎刃而解~~~
上一篇文章讲了 JavaScript 执行机制与作用域模型的底层逻辑解析
感兴趣的掘u们可以去学习学习 ˶ᵒ ᵕ ˂˶