2、变量提升与函数提升的编译原理剖析

58 阅读3分钟

💡 本篇目标:彻底理解 JS 编译阶段的“变量提升”和“函数提升”机制。并通过抽象语法树(AST)模拟解析、错误案例分析,揭开函数执行前的幕后真相。


🎯 一、什么是“变量提升”?

📖 定义:

JavaScript 在执行代码前,会进行一个“预编译”过程,将 var 声明的变量 提升到作用域顶部,但不会赋值

console.log(a) // undefined ❗
var a = 10

等价于:

var a
console.log(a)
a = 10

🧠 二、执行阶段划分:编译阶段 VS 执行阶段

阶段发生时间发生什么
编译阶段代码执行前变量、函数声明记录到作用域中
执行阶段自上而下按顺序执行语句、计算表达式等

✅ 编译阶段:

  • 找到所有 varfunction 声明
  • 把变量注册进当前执行上下文的环境记录中

🧩 三、函数提升优先级更高

foo() // ✅ 正常输出 'foo'

function foo() {
  console.log('foo')
}
  • 函数声明在编译阶段会完整提升,包括函数体
  • 所以执行 foo() 时已准备就绪

🔥 四、函数表达式 vs 函数声明

bar() // ❌ TypeError: bar is not a function

var bar = function () {
  console.log('bar')
}

原因分析:

var bar // 编译阶段:仅提升变量声明
bar()    // 执行阶段:bar 是 undefined,调用报错
bar = function () {...}

⚠️函数表达式不会提升函数体,只会提升变量定义!


📦 五、let/const 不存在“变量提升”?是“暂时性死区”!

console.log(a) // ❌ ReferenceError
let a = 1

虽然 a 在词法环境中是存在的,但在执行到 let a = 1 之前,处于“暂时性死区(TDZ)”。

🔍 内部原理:

  • 在词法环境中创建 a,但设置为 “uninitialized”
  • 在进入初始化语句之前,访问会抛异常

✅ 六、工程实战案例:为什么一些代码会出现在顶部?

示例:ES5 模拟模块封装器

(function() {
  var version = '1.0.0'
  function logVersion() {
    console.log(version)
  }
  logVersion()
})()

即使我们写在底部,变量提升保证了它在执行时已经可用。

(function() {
  logVersion()         // ✅ 变量提升
  function logVersion() {
    console.log(version)
  }
  var version = '1.0.0'
})()

📌 注意:虽然 version 被提升,但只提升了声明,不包含赋值


🧨 七、常见错误与注意点

❌ 使用 var 造成的“覆盖 bug”

var a = 1
function test() {
  console.log(a) // undefined
  var a = 2
}
test()

原因:函数作用域内的 var a 被提升,覆盖了外部变量。


❌ 函数声明被 var 覆盖

function test() {
  console.log(foo) // function foo() {...}
  var foo = 123
  console.log(foo) // 123
  function foo() {
    return 'bar'
  }
}
test()

解释:

  1. 函数 foo 声明先被注册
  2. 然后 var foo 覆盖函数标识符(不影响函数体)
  3. 执行阶段 foo 是 123

🧪 八、AST(抽象语法树)视角理解提升顺序

源码:

var a = 1
function b() {}

解析后的提升逻辑(简化):

GlobalExecutionContext = {
  LexicalEnvironment: {
    a: undefined,
    b: function() {}
  }
}

你可以用 esprima 或 Babel Parser 查看 AST 结构。


🧰 九、最佳实践建议

场景建议
新项目使用 let/const 避免 TDZ 问题
函数表达式定义在使用之前
模块封装了解提升逻辑避免被覆盖
Debug使用 Chrome DevTools 的 Scope 面板查看作用域变化

🧭 总结回顾

  • JS 执行前有“预编译”阶段,变量与函数声明提升
  • functionvar 拥有更高优先级
  • 函数表达式只提升变量,函数体不会被提升
  • let/const 不可提前访问,存在“暂时性死区”
  • 掌握提升逻辑能有效避免难以发现的 bug

📘 下一篇《第3篇:闭包到底闭了谁:项目中闭包的5个应用与陷阱》,将深入 JS 中最经典又最容易误用的闭包机制,手写几个经典案例与内存泄漏分析。