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

搞懂 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 有暂时性死区
相关推荐
用户852495071841 小时前
Prompt 就像点外卖:写清楚"备注",AI 才能给你送对"菜
程序员
黄敬峰1 小时前
从 Python 模块化到提示词工程:构建高效的 LLM 交互流程
程序员
Larcher2 小时前
JS 变量提升:代码没动,为什么执行顺序就变了?
前端·javascript·前端框架
就叫_这个吧3 小时前
JavaScript中常用事件示例展示附源码
开发语言·javascript·html
代码N年归来仍是新手村成员3 小时前
【AWS】Lambda 初识与服务部署
javascript·react.js·ai·node.js·云计算·ai编程·aws
云水一下3 小时前
JavaScript 从零基础到精通系列:流程控制、函数与作用域
前端·javascript
丷丩3 小时前
MapLibre GL JS第28课:PMTiles源和协议
javascript·gis·map·mapbox·maplibre gl js
之歆4 小时前
Day24_JavaScript正则表达式与性能优化实战:从入门到精通
javascript·性能优化·正则表达式
柚子科技4 小时前
Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了
前端·javascript·vue.js