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

搞懂 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 有暂时性死区
相关推荐
不听话坏4 小时前
Ignition篇(下 一) 动态执行前的事情
开发语言·前端·javascript
小二·5 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
阿里嘎多学长7 小时前
2026-07-10 GitHub 热点项目精选
开发语言·程序员·github·代码托管
CHNE_TAO_EMSM7 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
竹林8189 小时前
从 ethers.js 迁移到 Viem:一个签名验证 Bug 让我彻底放弃旧爱
javascript
你挚爱的强哥10 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
半个落月10 小时前
用 LangChain.js 手写一个能读写文件和执行命令的 Mini Cursor
javascript·人工智能·后端
天外天-亮10 小时前
HBuilder X 使用 uview-plus 方式
开发语言·javascript·hbuilder x·uview-plus
我有满天星辰12 小时前
Vue项目脚手架之争:create-vue与Vue CLI全面对比
前端·javascript·vue.js
多加点辣也没关系15 小时前
JavaScript|第9章:对象 — 基础
开发语言·javascript·ecmascript