搞懂 JS 变量提升:从诡异现象到底层原理,一篇吃透
你是不是也写过这种反直觉的 JS 代码?
javascript
showName() // 函数被执行了
console.log(myname) // undefined
var myname = '极客时间'
function showName() {
console.log('函数被执行了')
}
没定义就调用函数不报错、变量没赋值就是 undefined、代码不是从上到下跑......这就是前端最经典、面试必考的 变量提升(Hoisting) 。
今天用最接地气的话,带你从执行流程 → 提升本质 → var/let/const 区别 → 实战避坑,一次性彻底搞懂。
一、先破误区:JS 不是一行一行执行的
很多新手以为:
JS 代码从上到下、一行一行顺序执行。
大错特错!
JS 代码执行分两个阶段,缺一不可:
- 编译阶段(执行前一瞬间)
- 执行阶段(真正跑代码)
变量提升,只发生在编译阶段,和执行阶段无关。
二、变量提升到底是什么?
JS 引擎在执行代码前 ,会先扫描一遍代码,把变量声明 和函数声明 提前 "登记" 到内存里,逻辑上挪到作用域顶部,但物理代码不会动。登记完,再按顺序执行代码。
一句话总结:声明提前,赋值原地;函数优先,变量靠后。
三、用一段代码,拆解完整执行流程
我们拿这段经典代码举例:
javascript
showName()
console.log(myname)
console.log(add) // undefined
var myname = '极客时间'
// 函数声明
function showName() {
console.log('函数被执行了')
}
// 函数表达式
var add = function (a, b) {
return a + b
}
第一步:编译阶段(引擎偷偷做的事)
引擎扫描后,在内存里生成执行上下文,大概长这样:
javascript
// 变量环境
var myname = undefined
var add = undefined
// 函数声明直接提升成函数对象
function showName() { ... }
var变量:提升声明,默认值undefined- 函数声明:整个函数提升,可以直接调用
- 函数表达式:只提升变量名,函数不提升
第二步:执行阶段(按代码顺序跑)
scss
// 执行
showName() // 有函数 → 正常执行
console.log(myname) // undefined
console.log(add) // undefined
// 赋值才真正生效
myname = '极客时间'
add = function(...) {}
所以你看到的结果是:
javascript
函数被执行了
undefined
undefined

四、3 种声明提升大对比:var /let/const
这是面试必考题,也是日常写代码最容易踩坑的地方。
1. var:老派选手,完全提升
- 提升:声明 + 初始化(
undefined) - 作用域:函数 / 全局作用域
- 声明前可访问,值为
undefined
css
console.log(a) // undefined
var a = 10
2. let/const:现代规范,只提升不初始化
- 提升:声明提升,但不初始化
- 存在暂时性死区 TDZ
- 声明前访问直接报错
- 块级作用域
{}有效
javascript
console.log(myname) // Uncaught ReferenceError
let myname = '极客时间'
3. 函数声明 vs 函数表达式
- 函数声明:整体提升,可提前调用 ✅
- 函数表达式 :只提升变量,提前访问是
undefined❌
javascript
运行
kotlin
fn() // 可以
function fn() {}
fun() // 报错
var fun = function() {}
五、经典坑点:for 循环 + setTimeout 秒懂
很多人被这道题坑过,本质就是作用域 + 提升。
用 var:全部输出 10
scss
for(var i = 0; i < 10; i++){
setTimeout(()=>{
console.log(i) // 全是 10
},10)
}
原因:var 是函数 / 全局作用域,i 被提升到外部,循环结束 i=10,异步执行时拿到的都是同一个 i。
用 let:完美输出 0-9
javascript
for (let i = 0; i < 10; i++) {
setTimeout(()=>{
console.log(i) // 0,1,2...9
},10)
}
原因:let 是块级作用域 ,每次循环都生成一个新 i,异步回调绑定当前块的 i。
六、一张表记住所有规则
表格
| 类型 | 是否提升 | 初始值 | 作用域 | 声明前访问 |
|---|---|---|---|---|
| var 变量 | ✅ | undefined | 函数 / 全局 | undefined |
| 函数声明 | ✅ | 函数对象 | 函数 / 全局 | 可正常调用 |
| let/const | ✅ | 无(TDZ) | 块级 | 直接报错 |
| 函数表达式 | ✅ | undefined | 函数 / 全局 | undefined |
七、实战建议:怎么写才不出 bug?
- 尽量用 let/const,少用 var
- 先声明,后使用,不要依赖提升
- 函数优先用声明,工具函数用表达式
- 循环异步优先用
let,避免作用域污染 - 理解提升不是为了钻空子,而是为了看懂底层、排查 bug
八、最后总结(背会这 4 句)
- JS 先编译 再执行,提升发生在编译阶段
- 声明提升,赋值不提升
- 函数声明 > 变量声明
- var 提升为
undefined,let/const 有暂时性死区