💡 本篇目标:彻底理解 JS 编译阶段的“变量提升”和“函数提升”机制。并通过抽象语法树(AST)模拟解析、错误案例分析,揭开函数执行前的幕后真相。
🎯 一、什么是“变量提升”?
📖 定义:
JavaScript 在执行代码前,会进行一个“预编译”过程,将
var声明的变量 提升到作用域顶部,但不会赋值。
console.log(a) // undefined ❗
var a = 10
等价于:
var a
console.log(a)
a = 10
🧠 二、执行阶段划分:编译阶段 VS 执行阶段
| 阶段 | 发生时间 | 发生什么 |
|---|---|---|
| 编译阶段 | 代码执行前 | 变量、函数声明记录到作用域中 |
| 执行阶段 | 自上而下 | 按顺序执行语句、计算表达式等 |
✅ 编译阶段:
- 找到所有
var、function声明 - 把变量注册进当前执行上下文的环境记录中
🧩 三、函数提升优先级更高
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()
解释:
- 函数 foo 声明先被注册
- 然后 var foo 覆盖函数标识符(不影响函数体)
- 执行阶段 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 执行前有“预编译”阶段,变量与函数声明提升
function比var拥有更高优先级- 函数表达式只提升变量,函数体不会被提升
let/const不可提前访问,存在“暂时性死区”- 掌握提升逻辑能有效避免难以发现的 bug
📘 下一篇《第3篇:闭包到底闭了谁:项目中闭包的5个应用与陷阱》,将深入 JS 中最经典又最容易误用的闭包机制,手写几个经典案例与内存泄漏分析。