JavaScript预编译指南:从新手到轻松掌握

136 阅读5分钟

JavaScript预编译指南:从新手到轻松掌握

理解预编译,让你的JS代码更清晰


前言

上一篇我们聊了作用域,知道了变量在哪里可用。但你知道吗?在代码执行前,JavaScript 还悄悄做了些“准备工作”——这就是今天要讲的预编译。

什么是预编译?一个餐厅的比喻

想象你要开一家餐厅:

· 预编译阶段:准备菜单、安排桌位、招聘厨师

· 执行阶段:客人点菜、厨师做菜、上菜

预编译就是 JS 引擎在执行代码前,先把变量、函数声明等“安排好座位”的过程。

一、什么时候会发生预编译?

主要有两种情况:

  1. 页面加载时:全局预编译
  2. 函数调用前:函数预编译
// 全局预编译:页面加载时就发生
var globalVar = "我在全局";

function test() {
  // 函数预编译:函数调用前发生
  console.log("函数内部");
}

test(); // 调用函数,触发函数预编译

二、预编译的四个步骤(重点!)

预编译主要做四件事,记住这个顺序:

  1. 创建执行上下文(Activation Object)

  2. 找形参和变量声明 → 属性名作为 AO 属性名,值为 undefined

  3. 形参和实参相统一

  4. 找函数声明 → 函数名作为 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预编译的?在学习过程中遇到过哪些困惑?欢迎在评论区分享你的想法和经验!

如果这篇文章对你有帮助,请点赞收藏支持一下,这是我继续写作的最大动力!