📌 1. 前言
上篇文章我们聊了 JS 的作用域和作用域链,知道了有全局作用域、函数作用域和块级作用域,也知道了变量查找是由内向外的。但你知道 JS 引擎在运行代码时偷偷做了什么吗?今天我们就来揭秘这个“幕后工作”——预编译!
先考考你,这段代码输出什么?🤔
a=10
console.log(a); // 输出?
var b=20;
function foo() {
console.log(b); // 输出?
console.log(a); // 输出?
var a=30;
console.log(a); // 输出?
}
var a;
foo();
你的答案是
10;20;30;20?
😭 错啦!正确答案是:10;20;undefined;30
为什么在foo()里第一次输出a时,不像输出b那样去全局找呢?这就是 JS 的预编译在搞事情啦!接下来一起探索它的奥秘吧!
📌 2. 执行上下文:代码的小宇宙
预编译和执行上下文息息相关。简单说:
执行上下文就是 JS 代码运行时的“小环境”,保存了代码执行需要的一切信息。
每当 JS 引擎执行一段代码(全局/函数/块),就会创建一个对应的执行上下文。
执行上下文的诞生过程 🎂
分为两个阶段:
🛠️ 1. 创建阶段(预编译发生在这里!)
JS 引擎会做三件事:
- 创建变量对象(记录变量和函数声明)
- 确定
this指向 - 确定作用域链
🚀 2. 执行阶段
JS 引擎开始干活:
- 给变量赋值(变量=值,函数表达式=函数体)
- 执行函数调用
- 一行行运行代码
💡 小提示:执行上下文 vs 作用域
- 执行上下文是动态的,代码执行时创建,用完就销毁。
- 作用域是静态的,代码写完就固定了,不会变。
📌 3. 执行栈:管理上下文的“小管家”
想象一个栈结构(先进后出),这就是执行栈,专门管理执行上下文。
流程很简单:
- 程序启动:创建全局执行上下文,压入栈底。
- 函数调用:创建该函数的执行上下文,压入栈顶,开始执行。
- 函数执行完:它的上下文从栈顶弹出,控制权还给下面的上下文。
- 程序结束:全局上下文弹出,栈清空。
📌 4. 预编译:JS 引擎的“小动作”
这就是关键啦!预编译发生在执行上下文的创建阶段,主要是处理变量和函数的“提升”(Hoisting)。
🌍 全局代码的预编译步骤:
- 创建全局执行上下文对象
- 扫描
var变量声明:属性名=变量名,值=undefined - 扫描函数声明:属性名=函数名,值=整个函数体
- 开始执行代码(赋值、函数调用等)
🧩 函数内部的预编译步骤:
- 创建函数执行上下文对象
- 扫描形参和
var变量:属性名=名字,值=undefined - 将实参值赋给形参
- 扫描函数体内的函数声明:属性名=函数名,值=函数体
- 开始执行函数内代码
✨ 用预编译分析开头的代码
a=10
console.log(a); // ?
var b=20;
function foo() {
console.log(b); // ?
console.log(a); // ?
var a=30;
console.log(a); // ?
}
var a;
foo();
- 全局预编译:
- 找到
var a;->a: undefined - 找到
var b;->b: undefined - 找到
function foo() {...}->foo: 函数体
全局上下文初始状态:{ a: undefined, b: undefined, foo: [function] }
- 找到
- 全局执行:
a = 10-> 全局上下文a变成10console.log(a)-> 输出10b = 20-> 全局上下文b变成20- 调用
foo()
foo函数预编译:- 找到
var a;->a: undefined(在foo自己的上下文中)
foo上下文初始状态:{ a: undefined }
- 找到
foo函数执行:console.log(b)->foo内没有b,去全局找 -> 全局b=20-> 输出20console.log(a)->foo内有a(值是undefined!)-> 输出undefined(关键!)a = 30->foo内的a变成30console.log(a)-> 输出30
🎉 所以最终输出是:10 -> 20 -> undefined -> 30
💎 预编译的核心:变量提升
var声明的变量和function声明的函数会提升(Hoisting)。- 提升后变量初始值是
undefined,函数则是整个函数体。 - 注意!
let/const没有变量提升!提前使用会报错(暂时性死区)。
📌 5. 来练练手!
试试分析这段代码,把答案打在评论区吧~ ✍️
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);