JavaScript预编译指南:从新手到轻松掌握
理解预编译,让你的JS代码更清晰
前言
上一篇我们聊了作用域,知道了变量在哪里可用。但你知道吗?在代码执行前,JavaScript 还悄悄做了些“准备工作”——这就是今天要讲的预编译。
什么是预编译?一个餐厅的比喻
想象你要开一家餐厅:
· 预编译阶段:准备菜单、安排桌位、招聘厨师
· 执行阶段:客人点菜、厨师做菜、上菜
预编译就是 JS 引擎在执行代码前,先把变量、函数声明等“安排好座位”的过程。
一、什么时候会发生预编译?
主要有两种情况:
- 页面加载时:全局预编译
- 函数调用前:函数预编译
// 全局预编译:页面加载时就发生
var globalVar = "我在全局";
function test() {
// 函数预编译:函数调用前发生
console.log("函数内部");
}
test(); // 调用函数,触发函数预编译
二、预编译的四个步骤(重点!)
预编译主要做四件事,记住这个顺序:
-
创建执行上下文(Activation Object)
-
找形参和变量声明 → 属性名作为 AO 属性名,值为 undefined
-
形参和实参相统一
-
找函数声明 → 函数名作为 AO 属性名,值为函数体
三、实战演练:看看预编译怎么工作
案例1:简单的预编译过程
function fn(a) {
console.log(a); // 输出:function a() {}
// 解析:预编译阶段函数声明提升,a指向函数体
var a = 123 // 执行:a被重新赋值为123,覆盖之前的函数引用
console.log(a); // 输出:123
// 解析:变量赋值生效,a现在是一个数字
function a() {} // 预编译已处理,执行阶段跳过
// 注意:函数声明在预编译时就已经"提升"到顶部
var b = function() {} // 执行:b被赋值为匿名函数
console.log(b); // 输出:function() {}
// 解析:函数表达式按顺序执行,不会提前提升
function c() {} // 预编译:函数声明提升
var c = a // 执行:c被赋值为a的当前值123
console.log(c); // 输出:123
// 解析:变量赋值覆盖了之前的函数声明
}
预编译阶段(创建 AO - Activation Object)
// 步骤1:创建 AO 对象
AO = {}
// 步骤2:处理形参和变量声明
AO = {
a: undefined, // 形参 a
b: undefined, // 变量 b
c: undefined // 变量 c
}
// 步骤3:形参和实参统一(假设调用 fn(1))
AO = {
a: 1, // 实参 1 赋值给形参 a
b: undefined,
c: undefined
}
// 步骤4:处理函数声明
AO = {
a: function a() {}, // 函数声明 a 覆盖了之前的 1
b: undefined,
c: function c() {} // 函数声明 c
}
预编译完成后的 AO:
AO = {
a: function a() {},
b: undefined,
c: function c() {}
}
执行过程分析
function fn(a) {
console.log(a); // 输出:function a() {}
// 当前 AO.a 的值是函数体
var a = 123 // 执行赋值:AO.a = 123
console.log(a); // 输出:123
// AO.a 现在被重新赋值为 123
function a() {} // 跳过(预编译阶段已处理)
var b = function() {} // 执行赋值:AO.b = function() {}
console.log(b); // 输出:function() {}
// AO.b 现在指向匿名函数
function c() {} // 跳过(预编译阶段已处理)
var c = a // 执行赋值:AO.c = AO.a = 123
console.log(c); // 输出:123
// AO.c 被重新赋值为 123,覆盖了之前的函数体
}
执行过程中的 AO 变化:
// 执行前(预编译后):
AO = { a: function a() {}, b: undefined, c: function c() {} }
// 执行 var a = 123 后:
AO = { a: 123, b: undefined, c: function c() {} }
// 执行 var b = function() {} 后:
AO = { a: 123, b: function() {}, c: function c() {} }
// 执行 var c = a 后:
AO = { a: 123, b: function() {}, c: 123 }
完整输出结果
function a() {}
123
function() {}
123
案例2:变量提升的真相
console.log(name); // 输出:undefined
var name = "掘金";
console.log(name); // 输出:"掘金"
// 实际上相当于:
var name; // 预编译:声明,值为 undefined
console.log(name); // 执行:输出 undefined
name = "掘金"; // 执行:赋值
console.log(name); // 执行:输出 "掘金"
案例3:变量声明 vs 函数声明
核心区别:提升的机制不同
1、函数声明——完全提升
console.log(sayHello); // 输出:function sayHello() {}
console.log(sayHello()); // 输出:"Hello"
function sayHello() {
return "Hello";
}
整个函数体都被提升到作用域顶部,可以在声明前调用,不会报错
2、函数表达式——只提升变量声明
console.log(sayHello); // 输出:undefined
// console.log(sayHello()); // 报错:sayHello is not a function
var sayHello = function() {
return "Hello";
};
console.log(sayHello()); // 输出:"Hello"
只有变量名 sayHello 被提升,值为 undefined,函数体留在原地,等待赋值,在赋值前调用会报错
要特别注意的部分:同名情况下的优先级
console.log(test); // 输出:function test() {}
function test() {
console.log("我是函数声明");
}
var test = "我是变量声明";
console.log(test); // 输出:"我是变量声明"
规则:函数声明 > 变量声明
四、全局预编译的特殊性
全局也有预编译,但有些要注意的:
· 创建的是 GO(Global Object) 而不是 AO
· 没有形参和实参统一这一步
· AO中的变量会遮蔽GO中的同名变量
预编译步骤对比
GO的预编译步骤:
// 1. 创建GO对象
GO = {}
// 2. 找变量声明
GO = { a: undefined }
// 3. 找函数声明
GO = { a: undefined, test: function test() {} }
// (无形参实参统一)
AO的预编译步骤:
function demo(a) {
var b = 2;
function c() {}
}
demo(1);
// 1. 创建AO
AO = {}
// 2. 找形参和变量声明
AO = { a: undefined, b: undefined }
// 3. 形参实参统一
AO = { a: 1, b: undefined }
// 4. 找函数声明
AO = { a: 1, b: undefined, c: function c() {} }
案例
var name = "全局name"; // GO中
function showName() {
console.log(name); // 输出:undefined
var name = "局部name"; // AO中的name遮蔽了全局的name
console.log(name); // 输出:"局部name"
}
showName();
console.log(name); // 输出:"全局name"
结语
预编译就像是JavaScript世界的“幕后导演”,它在代码执行前精心安排好所有变量和函数的“登场顺序”,理解它就能洞悉代码运行的底层逻辑。
你是如何理解JavaScript预编译的?在学习过程中遇到过哪些困惑?欢迎在评论区分享你的想法和经验!
如果这篇文章对你有帮助,请点赞收藏支持一下,这是我继续写作的最大动力!