在日常开发和面试中,JavaScript 的“预编译”机制(也称为变量提升、函数提升)经常成为考点。很多初学者会被一些看似“诡异”的输出结果困扰,其实背后都离不开 JS 引擎的预编译过程。本文将带你系统梳理 JS 预编译的原理、流程,并结合代码案例,帮助你彻底搞懂这一机制。
一、什么是 JS 预编译?
JavaScript 是一门解释型语言,但现代引擎(如 V8)在执行 JS 代码前,会先进行“编译”阶段。这个阶段主要做两件事:
- 变量和函数声明提升:提前收集所有的变量声明(
var)和函数声明(function),并在作用域顶部进行初始化。 - 创建执行上下文:为全局或函数作用域创建对应的执行上下文对象(ECO),用于存储变量、函数、参数等信息。
简而言之,JS 代码的执行分为两个阶段:预编译阶段 和 执行阶段。
二、全局与函数的预编译流程
1. 全局预编译
- 创建全局执行上下文对象(Global Object,简称 GO)。
- 找出所有全局变量声明(
var),在 GO 上创建同名属性,初始值为undefined。 - 找出所有全局函数声明,函数名作为 GO 的属性,值为函数体(后声明会覆盖前声明)。
2. 函数预编译
- 创建函数执行上下文对象(AO)。
- 形参和变量声明提升,初始值为
undefined。 - 实参赋值给形参。
- 函数声明提升,函数名作为 AO 属性,值为函数体(后声明会覆盖前声明)。
注意:变量提升只提升声明,不提升赋值;函数声明整体提升,函数表达式只提升变量名。
三、代码案例详解
案例1:变量和函数提升
function fn(a) {
console.log(a);
var a = 123;
console.log(a);
function a() { }
var b = function () { }
console.log(b);
function d() { }
var d = a;
console.log(d);
}
fn(1);
预编译过程:
- 创建 AO:
{ a: undefined, b: undefined, d: undefined } - 形参赋值:
a = 1 - 函数声明提升:
a = function a() {},d = function d() {} - 变量声明已存在,不变
- 执行阶段依次输出:
console.log(a)输出:function a() {}a = 123,console.log(a)输出:123b = function () {},console.log(b)输出:function () {}d = a,console.log(d)输出:123
案例2:参数、变量、函数同名
function foo(a, b) {
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b() { }
console.log(b);
}
foo(1);
预编译过程:
- AO 初始:
{ a: 1, b: undefined, c: undefined } - 函数声明提升:
b = function b() {} - 变量声明已存在,不变
- 执行阶段:
console.log(a)输出:1c = 0,var c已声明,赋值为 0a = 3,b = 2console.log(b)输出:2function b() {}已被b = 2覆盖console.log(b)输出:2
四、常见面试陷阱与注意点
- 函数声明优先于变量声明提升,但变量赋值会覆盖函数声明。
- 函数表达式只提升变量名,不提升函数体。
- 同名参数、变量、函数声明时,最终以最后一次声明为准。
- 全局变量未用 var 声明时,会自动挂载到 window(或 global)对象上,但不推荐这样做。
六、总结
JS 预编译机制是理解变量提升、函数提升、作用域链等核心概念的基础。掌握预编译流程,不仅能帮助你写出更健壮的代码,还能在面试中游刃有余。建议大家多写多练,遇到输出“诡异”的题目时,先在脑海中模拟一遍预编译过程,答案自然就清晰了。