【JavaScript】⚡ 秒懂 JS 预编译:变量提升的魔法原理大公开!

561 阅读4分钟

📌 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 引擎会做三件事:

  1. 创建变量对象(记录变量和函数声明)
  2. 确定 this 指向
  3. 确定作用域链
🚀 2. 执行阶段

JS 引擎开始干活:

  1. 给变量赋值(变量=值,函数表达式=函数体)
  2. 执行函数调用
  3. 一行行运行代码

💡 小提示:执行上下文 vs 作用域

  • 执行上下文是动态的,代码执行时创建,用完就销毁。
  • 作用域是静态的,代码写完就固定了,不会变。

📌 3. 执行栈:管理上下文的“小管家”

想象一个栈结构(先进后出),这就是执行栈,专门管理执行上下文。

流程很简单:

  1. 程序启动:创建全局执行上下文,压入栈底。
  2. 函数调用:创建该函数的执行上下文,压入栈顶,开始执行。
  3. 函数执行完:它的上下文从栈顶弹出,控制权还给下面的上下文。
  4. 程序结束:全局上下文弹出,栈清空。

📌 4. 预编译:JS 引擎的“小动作”

这就是关键啦!预编译发生在执行上下文的创建阶段,主要是处理变量和函数的“提升”(Hoisting)。

🌍 全局代码的预编译步骤:

  1. 创建全局执行上下文对象
  2. 扫描 var 变量声明:属性名=变量名,值= undefined
  3. 扫描函数声明:属性名=函数名,值=整个函数体
  4. 开始执行代码(赋值、函数调用等)

🧩 函数内部的预编译步骤:

  1. 创建函数执行上下文对象
  2. 扫描形参和 var 变量:属性名=名字,值= undefined
  3. 将实参值赋给形参
  4. 扫描函数体内的函数声明:属性名=函数名,值=函数体
  5. 开始执行函数内代码

✨ 用预编译分析开头的代码

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();
  1. 全局预编译
    • 找到 var a; -> a: undefined
    • 找到 var b; -> b: undefined
    • 找到 function foo() {...} -> foo: 函数体
      全局上下文初始状态: { a: undefined, b: undefined, foo: [function] }
  2. 全局执行
    • a = 10 -> 全局上下文 a 变成 10
    • console.log(a) -> 输出 10
    • b = 20 -> 全局上下文 b 变成 20
    • 调用 foo()
  3. foo 函数预编译
    • 找到 var a; -> a: undefined (在 foo 自己的上下文中)
      foo 上下文初始状态: { a: undefined }
  4. foo 函数执行
    • console.log(b) -> foo 内没有 b,去全局找 -> 全局 b=20 -> 输出 20
    • console.log(a) -> foo 内有 a(值是 undefined!)-> 输出 undefined (关键!)
    • a = 30 -> foo 内的 a 变成 30
    • console.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);