为什么变量能 未定义先使用?

搞懂 JS 变量提升:从诡异现象到底层原理,一篇吃透

你是不是也写过这种反直觉的 JS 代码?

javascript 复制代码
showName() // 函数被执行了
console.log(myname) // undefined
var myname = '极客时间'
function showName() {
  console.log('函数被执行了')
}

没定义就调用函数不报错、变量没赋值就是 undefined、代码不是从上到下跑......这就是前端最经典、面试必考的 变量提升(Hoisting) 。

今天用最接地气的话,带你从执行流程 → 提升本质 → var/let/const 区别 → 实战避坑,一次性彻底搞懂。


一、先破误区:JS 不是一行一行执行的

很多新手以为:

JS 代码从上到下、一行一行顺序执行。

大错特错!

JS 代码执行分两个阶段,缺一不可:

  1. 编译阶段(执行前一瞬间)
  2. 执行阶段(真正跑代码) 变量提升,只发生在编译阶段,和执行阶段无关。

二、变量提升到底是什么?

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?

  1. 尽量用 let/const,少用 var
  2. 先声明,后使用,不要依赖提升
  3. 函数优先用声明,工具函数用表达式
  4. 循环异步优先用 let,避免作用域污染
  5. 理解提升不是为了钻空子,而是为了看懂底层、排查 bug

八、最后总结(背会这 4 句)

  1. JS 先编译 再执行,提升发生在编译阶段
  2. 声明提升,赋值不提升
  3. 函数声明 > 变量声明
  4. var 提升为 undefined,let/const 有暂时性死区
相关推荐
SimonKing10 分钟前
Workbuddy太占C盘空间了,写个迁移工具迁走10G的数据
java·后端·程序员
挖掘狂人44 分钟前
Token 都上双十一货架了,AI 电脑的账该怎么算?
大数据·程序员·ai编程
深蓝AI4 小时前
26.1% 的 Agent 技能带漏洞:skills 霸榜 GitHub 当天,NVIDIA 和 OpenAI 同时掏出安检仪
人工智能·程序员
程序员cxuan14 小时前
离谱,AI 大佬顺手搓了个 YouTube ,而我顺手给他搓了个中文插件
人工智能·后端·程序员
1-1=015 小时前
Vue 3 模板语
前端·javascript·vue.js
风卿16 小时前
看着AI写代码太孤独了,来欣赏星云吧:一个DSH 装饰插件
前端·javascript
牧艺16 小时前
弹层不用再手算坐标了:CSS Anchor Positioning 从原理到可上线
前端·javascript·css
愚坤17 小时前
前端真有意思,JS还能独立做项目?
前端·javascript·github
李纲明18 小时前
微信小程序 getSystemInfo 提示已废弃、屏幕高度算错胶囊还对不齐?getWindowInfo 和 getDeviceInfo 怎么接才稳
javascript·微信小程序·小程序·小程序开发·devdiy
FungLeo19 小时前
成为全栈·Vue 管理后台篇·用 fetch 建一层可控的请求内核
前端·javascript·vue.js·http·fetch·formdata·接口封装