说实话,JavaScript 里的变量声明,看起来简单,但真到了写代码的时候,尤其是涉及到var、let和const的选择,不少工作了一两年的朋友可能还在凭感觉用。我刚开始学的时候,总觉得“能跑就行”,直到有一次因为var的变量提升,在循环里出了个很难排查的Bug,才痛下决心要把这哥仨彻底搞清楚。
今天咱们不背概念,直接从底层机制和实际场景出发,把这层窗户纸捅破。看完这篇,以后面试官再问,你不仅能答出区别,还能顺便讲讲暂时性死区和块级作用域,直接拿捏。
1. 变量提升(Hoisting):var 的“历史遗留问题”
先来看一段代码,你觉得会输出什么?
console.log(a); // undefined var a = 10;
在很多语言里,这肯定直接报错“未定义”。但在 JavaScript (ES5)里,它居然输出undefined。这就是变量提升在作祟。
JavaScript 引擎在编译阶段,会把var声明的变量和function声明挪到当前作用域的顶部。所以上面的代码在引擎眼里,其实是这样的:
var a; // 声明被提升,初始值为 undefined console.log(a); // undefined a = 10; // 赋值留在原地
var就这么个毛病:声明提升了,但赋值没动。如果你在赋值前访问它,拿到的就是undefined,而不是报错。这很容易导致一些隐蔽的 bug,比如变量名拼错了,结果拿到的却是undefined,程序还继续往下跑,最后在某个角落炸掉。
2. 块级作用域:let 和 const 带来的“新世界”
let和const是 ES6 引入的,它们解决了var最大的痛点——没有块级作用域。
以前用var,只有全局作用域和函数作用域。比如:
if (true) { var b = 20; } console.log(b); // 20,变量污染了外部这在for循环里特别坑:
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出 3, 3, 3因为i是全局的,循环结束后i变成 3,三个定时器都打印 3。
换成let试试:
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出 0, 1, 2let是块级作用域,每次循环都会生成一个新的块,把i“锁”在里面。所以定时器拿到的每个i都是独立的副本。
const也一样,声明的变量只在当前大括号{}内有效。
3. 暂时性死区(TDZ):let 和 const 的“严格模式”
你可能听过“let没有变量提升”这个说法。其实严格来说,let和const也有提升,但引擎对它们有一个限制:在声明之前,你不能访问它们。
console.log(c); // ReferenceError: Cannot access 'c' before initialization let c = 30;
从进入当前作用域开始,到变量声明完成之前,这块区域就叫暂时性死区(Temporal Dead Zone, TDZ)。
let x = 10; if (true) { // 这里就进入了 TDZ console.log(x); // ReferenceError let x = 20; // 声明在此 }哪怕外面有个同名的x,在块里面声明之前,这个x也是访问不到的。这其实是个好事,强制我们把变量声明放在使用之前,代码更健壮。
4. const 的“不变”陷阱
很多人以为const定义的是常量,不能修改。这句话对了一半。
const PI = 3.14; PI = 3.1415; // TypeError: Assignment to constant variable.
基本类型(数字、字符串、布尔)确实不能改。但const真正保护的是变量的引用地址,而不是值本身。
const person = { name: '张三' }; person.name = '李四'; // 允许!因为引用没变 person = {}; // 报错!尝试改变引用所以,如果你用const声明一个对象,你是可以修改对象内部属性的,但不能把整个对象重新赋值。
5. 实际开发中到底怎么选?
这个问题我问过很多同事,总结下来有一套比较实用的“心法”:
默认用
const:除非你明确知道这个变量后面需要重新赋值,否则一律const。这能防止意外篡改,也让代码的意图更清晰。需要重新赋值时用
let:比如循环计数器、累加变量、或者后续逻辑会改变的变量。尽量别用
var:在现在的工程化项目里,var基本可以退休了。除非你要兼容远古浏览器(比如 IE11),否则没必要再用它。
我自己的习惯是,写 React 组件时,导入的模块用const,状态用let,永远不用var。
6. 一张图帮你理清全局对象
还有一个冷知识:在全局作用域下,var声明的变量会成为window(浏览器)或global(Node)的属性,而let和const不会。
var a = 1; let b = 2; console.log(window.a); // 1 console.log(window.b); // undefined
这其实也解释了为什么var容易造成全局污染。如果你正在写一个工具库,一不小心用var声明了个公共变量,很可能就把别人的同名变量覆盖了。
总结一下
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 全局/函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是(初始undefined) | 是(但存在 TDZ,不初始化) | 是(但存在 TDZ,不初始化) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 修改值 | 允许 | 允许 | 基本类型不可改,引用类型可改属性 |
挂载到window | 是 | 否 | 否 |
其实这背后的核心逻辑,就是 JavaScript 在设计上越来越严谨和安全。var的宽松设计在早期确实方便,但带来的维护成本太高。let和const的引入,本质上是在帮你养成好习惯。
下次面试官再问这个问题,你可以先抛出变量提升和 TDZ 的区别,再结合for循环的经典例子说明块级作用域的好处,最后提一嘴const的引用赋值。这一套组合拳下来,相信对方心里已经有数了。
好了,关于这三者的区别就聊到这儿。如果你在项目中因为变量声明踩过什么坑,也欢迎在评论区分享出来,大家一起避避雷。觉得有用的话,点个赞再走~