你不知道的JavaScript 编译机制:作用域与声明提升的深度解析

1,460 阅读6分钟

前言

在 JavaScript 的世界里,V8 引擎就像一位幕后导演,对代码进行 “编译 + 执行” 的两步走演绎。这其中,声明提升执行上下文是核心剧情,让我们用色彩与细节来拆解这场精彩的表演。

请坐上学习的快车,前方高能来袭!

一、编译先行:V8 引擎的 “剧本预处理”

当 V8 读取 JS 代码时,会先进入编译阶段,把所有声明部分(变量声明、函数声明)悄悄 “提升” 到当前作用域的顶部。这就像话剧开演前,道具和演员先就位 —— 代码执行前,声明已经在 “幕后” 准备好了。

(一)函数执行上下文:AO 对象的诞生

当函数体被编译时,会创建函数的执行上下文,对应一个 “演员休息室”——AO(Activation Object,活动对象) 。它的诞生遵循以下步骤:

  1. 找形参和变量声明:把形参和 var 声明的变量作为 AO 的属性,初始值为 undefined
  2. 实参形参统一:如果调用函数时传了实参,就把实参的值赋给对应的形参属性。
  3. 找函数声明:把函数声明的名称作为 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分析如下:

image.png

运行结果和我们预期的一致:

image.png

在 AO 中,变量和函数的 “就位顺序” 是:函数声明 > 变量声明,所以函数声明会覆盖同名变量的早期定义。

(二)全局执行上下文:GO 对象的统治

那么聊完 AO 对象,接下来该 GO 登场啦!当全局代码被编译时,会创建全局执行上下文,对应 “全局后台”——GO(Global Object,全局对象) 。它的诞生步骤是:

  1. 变量声明(var 声明) :把变量名作为 GO 的属性,初始值为 undefined
  2. 函数声明:把函数名作为 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 分析如下:

image.png

运行结果也和分析的一致:

image.png

在 GO 中,函数声明的优先级高于变量声明,但如果变量有赋值操作,最终值会以赋值为准。

二、用 “老板与秘书” analogy 秒懂 V8 执行逻辑

在之前对编译阶段、AO/GO 对象的解析基础上,我们可以用 “老板与秘书” 的生活化 analogy,把 V8 引擎的 “编译 - 执行” 流程讲得更生动。

把 V8 引擎想象成一家公司,编译部门是 “秘书”执行部门是 “老板” ,而 JavaScript 代码就是 “待处理的工作任务”。

  • 秘书(编译部门) :接到任务(代码)后,先做 “预处理” —— 把所有变量声明、函数声明提前整理好,记在 “工作备忘录”(AO/GO 对象)里,确保老板问起时能立刻回答。
  • 老板(执行部门) :只负责按顺序处理具体业务(执行代码的赋值、运算、输出等操作),但遇到 “变量 / 函数” 时,会先问秘书 “这个东西之前记了吗?”(即从 AO/GO 中查找定义)。

就拿 AO 的来代码举例子:

function fn(a) {}  // 函数声明
fn(1)              // 函数调用
  1. 秘书(编译部门)预处理函数声明:当秘书看到 function fn(a){} 时,立刻在 “全局备忘录(GO)” 里记下:fn 是一个函数,内容是 function fn(a) {}
  2. 老板(执行部门)执行函数调用:当老板执行到 fn(1) 时,先问秘书:“fn 是什么?”秘书翻开备忘录(GO):“是个函数,这就给您调过来执行!”于是函数 fn 开始执行 —— 此时秘书又会为这个函数单独建一个 “部门备忘录(AO)” ,记录函数内的形参 a、变量声明、函数声明等信息,供函数执行过程中查询。

上图更清晰!

image.png

再延伸到变量提升的场景:

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吧

先自己分析再看运行结果哦~

image.png

image.png

三、核心结论:编译阶段的 “潜规则”

  • 声明提升只提升 “名称”var 声明的变量提升后值为 undefined,函数声明提升后值为完整函数体。
  • 作用域内优先级:函数声明 > 变量声明;执行阶段的赋值操作会覆盖声明阶段的定义。
  • AO 与 GO 的边界:函数执行时创建 AO,全局代码执行时创建 GO,二者相互隔离又通过作用域链关联。

总结

✨ 核心收获:吃透编译机制,直抵 JS 执行本质 ✨

掌握 声明提升优先级(函数声明 > 变量声明)、AO/GO 上下文创建规则 这些编译阶段的 “底层潜规则”,就像手握 JS 执行逻辑的 “透视镜”—— 既能精准看透变量 / 函数的 “提前就位” 逻辑,又能清晰划分作用域边界,从此告别变量提升导致的 undefined 困惑、作用域混淆引发的取值异常等 “坑”,让代码执行流程了然于胸,疑难问题迎刃而解~~~

上一篇文章讲了 JavaScript 执行机制与作用域模型的底层逻辑解析

感兴趣的掘u们可以去学习学习 ˶ᵒ ᵕ ˂˶