99%的 js 程序员都会陷入的误区,你真的了解声明提升和暂时性死区吗?

24 阅读7分钟

如果你写了几年JS ,你一定对变量提升、暂时性死区(TDZ) 不陌生

但绝大多少人的认知,从根本上就是错的

❌ 误区1:let/const 没有变量提升,只有 var 才有提升

❌ 误区2:暂时性死区 = 没有提升,只是语法限制

❌ 误区3:变量提升是代码被“移到了顶部”

❌ 误区4:函数提升优先级永远高于变量提升

这些认知错位,平时写业务代码是可能察觉不到,但一旦遇到面试,复杂闭包,作用域嵌套bug,瞬间就会崩盘

今天这篇文章,彻底扒干净 JS 声明提升和 TDZ的底层真相,原因居然是这个,推翻全网 99%的错误解读,一次性根治所有相关bug 和面试盲点

一.破除核心误区:let/const真的没有提升吗?果真吗?

全网流传最广的一句话:

var 有提升,let/const没有提升

这句话是错的!大错特错!

精确真相: let/const完全存在声明提升,只是被暂时性死区锁住你,禁止访问

有人可能会说闹麻了,let/const不就是通过TDZ实现不声明提升的效果吗?表层效果和“无提升”一模一样:声明前访问直接报错,所以结果是没有提升。

非也,非也

这一段代码,可以直接击穿这个错误认知: 击穿这个错误认知:

var a = 100;


if (true) {
  // 报错:Cannot access 'a' before initialization
  console.log(a);
  let a = 200;
}

我们来逻辑推演:

  1. 如果 let a真的没有提升:当前块作用域没有 a,引擎会向上层全局作用域查找,打印100,不会报错;

  2. 现实是直接报错,唯一的解释:

let a 在进入块作用域的瞬间,就已经提升到当前作用域顶部了,并且被 TDZ 锁定,屏蔽了外层的全局变量 a。

这就是提升的铁证

所谓的 TDZ,不是取消了提升,而是给提升加了一把锁

二.彻底分清:无提升 VS 有提升+TDZ 的核心本质,你们知道吗?

这是99%程序员分不清的核心知识点,我用极简逻辑讲透:

1.真正的无提升(Java/Python/C等绝大多数语言)

引擎逐行执行,走到声明行,才会识别这个变量 声明之前:完全不认识这个变量,会向上层查找同名变量,无任何锁定效果

2.JS「let/const 提升 + TDZ 锁死」

预编译阶段,变量就已经提升到当前作用域顶部,引擎已经识别到了该变量

只是在声明代码行执行前,变量处于暂时性死区

✅ 变量已注册(提升完成)

✅ 屏蔽外层同名变量

❌ 禁止任何读取、访问操作

3. JS 「var 提升」

提升后挂载到 VO 变量对象,赋值 undefined,无死区限制,声明前可以正常访问。

三.提升不是“代码被移到顶部”

新手教程最爱说:变量、函数会被提升,移动到代码最顶部

这是通俗错误解读,底层根本不是这样!

精准底层真相:

提升是 JS 预编译阶段的「变量登记机制」,不是代码位置移动

JS 执行分两个阶段:

阶段1:预编译(瞬间完成)

引擎扫描当前作用域所有声明:

  1. 扫描所有 var 变量,登记到 VO/AO,赋值 undefined

  2. 扫描所有 function 函数声明,登记到 VO/AO,赋值完整函数体;

  3. 扫描 let/const,完成提升,进入 TDZ 锁定状态。

阶段2:逐行执行

代码位置完全不变,只是引擎提前认识了所有变量。

如果是代码真的“被移动到顶部”,会产生无数逻辑bug,JS 引擎从来不会修改你的代码位置。

四.全网最易混: var、let、function 提升优先级终极规则

很多人背的“函数提升优先于变量提升”是半截真理,不完整,极易踩坑。

完整、可落地、100%准确的提升顺序(预编译固定流程):

1.变量提升(var/let/const全部先登记)

2.函数声明提升(后覆盖)

实战经典代码:

console.log(a); 
var a = 10;
function a() {}

执行结果:[Function: a]

推演过程:

  1. 预编译第一步:扫描 var 变量,VO.a = undefined

  2. 预编译第二步:扫描函数声明,VO.a = 覆盖为完整函数;

  3. 执行阶段:打印 a,得到函数;

  4. 执行 var a = 10,a 被赋值为 10。

再看 let 场景(彻底击穿误区):

console.log(b);
let b = 20;
function b() {}

结果:直接报错

原因:

  1. let b 先提升,进入 TDZ 死区;

  2. 后续函数声明无法覆盖死区变量;

  3. 声明前访问,触发 TDZ 报错。

这也再次证明:let 绝对有提升,否则函数提升会生效,不会报错

五、暂时性死区(TDZ)的真正设计目的

很多人不知道,TDZ 是 ES6 为了修复 var 的历史缺陷,被迫新增的机制。

我们复盘 JS 发展史,瞬间通透:

  1. JS 初期为了兼容新手,设计了变量提升,允许先使用后声明,降低入门难度;

  2. 但 var 无块级作用域、无死区,提升后提前访问得到 undefined,产生大量隐秘bug;

  3. ES6 不能直接废除提升(会导致所有老项目报错);

  4. 最终折中方案:

保留 let/const 的提升机制(兼容底层逻辑),新增 TDZ 暂时性死区,强制「先声明、后使用」,规范编码规范

TDZ 核心作用总结:

✅ 解决 var 变量提升带来的诡异 undefined 问题

✅ 杜绝变量在声明前被使用,代码逻辑更严谨

✅ 屏蔽外层同名变量,解决作用域变量污染问题

✅ 配合块级作用域,彻底解决 var 的所有历史弊端

所以在使用变量时,一般都是const>let>var ,没有特殊情况,千万不要用var!

六、终极对比表(一张表吃透所有差异)

声明方式是否提升是否有TDZ声明前访问结果作用域
var✅ 提升❌ 无undefined函数/全局作用域
let✅ 提升✅ 有报错(死区锁定)块级作用域
const✅ 提升✅ 有报错(死区锁定)块级作用域
function✅ 优先提升❌ 无可直接调用对应作用域

七、全文核心总结(理解大于记忆)

  1. 最大误区纠正:let/const 绝对有声明提升,只是被 TDZ 锁住,并非无提升;

  2. TDZ 本质:提升后的变量临时锁定、禁止访问,声明行执行后解锁;

  3. 提升本质:预编译阶段的变量登记机制,不是代码位置移动;

  4. 提升优先级:变量先登记,函数后覆盖,let 死区变量不可被覆盖;

  5. 设计初衷:TDZ 是 ES6 为兼容历史、修复 var 漏洞的最优但也是无奈的折中方案;

  6. 核心区别:无提升会向外找变量,有提升+TDZ会屏蔽外层变量并报错。

写在最后

很多人学 JS 只会记表层效果,不懂底层原理,所以永远踩坑。

声明提升和 TDZ,看似简单,却是 JS 作用域、预编译、闭包的底层基石

吃透这两个知识点,你对 JS 执行机制的理解,已经超过99%的普通开发者。

觉得本文干货硬核,恳请点赞 + 收藏 + 转发,顺手点个关注不迷路,下篇硬核剖析声明提升 JS 为什么非要设计出「声明提升」这种反直觉特性?

别的语言都没有,为什么唯独 JS 要有?

从 10 天仓促诞生,到 ES6 用 TDZ 补丁救场,背后藏着怎样的历史渊源和语言设计哲学

敬请期待吧!!!