从var到const:彻底搞懂 ES6 变量声明的进化之路
你有没有遇到过这样的情况?在for循环里写了一堆setTimeout,结果回调输出的全是同一个值。或者在一个if块里定义了一个变量,却发现外面也能访问?如果你曾被这些问题困扰,那很可能是因为你还停留在var的时代。
JavaScript 的变量声明机制,在 ES6(ECMAScript 2015)之前其实一直是个“坑”。而let和const的出现,不是简单的语法糖,而是对语言根基的一次重构。它们解决了长期存在的作用域混乱问题,让 JS 更接近现代编程语言应有的样子。
今天我们就来一次讲透:为什么let和const必须取代var?它们到底改变了什么?又该如何正确使用?
一、var到底哪里不对劲?
要理解let和const的价值,得先看看var有多“反直觉”。
1. 变量提升:代码还没执行,变量就已经“存在”了?
console.log(a); // 输出 undefined,而不是报错 var a = 10;这行代码能运行,但结果可能让你懵——明明还没定义a,怎么不报错反而输出undefined?
原因就是变量提升(Hoisting):JS 引擎在执行前会把所有var声明“提到”作用域顶部,相当于:
var a; console.log(a); // undefined a = 10;这种行为容易让人误以为变量已经初始化了,但实际上它只是被声明了,值还是undefined。
2. 函数作用域 vs 块级作用域:if块不是“隔离区”
if (true) { var x = 'I am visible outside'; } console.log(x); // 能打印出来!在大多数编程语言中,if或for里的变量只在花括号内有效。但在 JS 中,var是函数作用域的,意味着只要不在函数内,它就会挂到全局或当前函数作用域下。
这就导致了变量“泄露”,尤其是在循环中:
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:3, 3, 3三个定时器都输出3,因为i是共享的,等回调执行时,循环早已结束,i的最终值是3。
这就是经典的“闭包陷阱”。
二、let:真正意义上的块级作用域
ES6 引入let,就是为了终结这些诡异行为。
✅ 块级作用域:出了{}就看不见
{ let blockScoped = 'hello'; } console.log(blockScoped); // ReferenceError: blockScoped is not defined现在变量真的只在{}内有效了。无论是if、for还是普通的代码块,let都会创建一个独立的作用域。
✅ 暂时性死区(TDZ):不再允许提前使用
console.log(y); // 报错!Cannot access 'y' before initialization let y = 20;和var不同,let虽然也会被“提升”,但处于暂时性死区(Temporal Dead Zone)—— 在声明之前访问会直接抛出ReferenceError,而不是返回undefined。
这个设计是故意的:它强迫开发者遵循“先声明后使用”的良好习惯,避免逻辑错误。
✅ 禁止重复声明
let z = 1; let z = 2; // SyntaxError: Identifier 'z' has already been declared在同一作用域内,不能重复用let声明同一个变量。这比var安全得多。
🔥 经典问题解决:for循环中的闭包
再看一遍那个令人头疼的例子:
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:0, 1, 2神奇吗?为什么这次对了?
关键点:let在每次迭代时都会创建一个新的绑定(binding),也就是说,每个i实际上都是不同的变量实例。因此每个setTimeout捕获的是当次循环的i,而不是共享同一个。
💡 小知识:你可以理解为
let i在每次循环时都被“重新声明”了一次,引擎自动为你做了作用域隔离。
三、const:不可变的承诺
如果说let是“可变的块级变量”,那const就是“不可重新赋值的常量”。
✅ 必须初始化,且不能重新赋值
const PI = 3.14159; PI = 3.14; // TypeError: Assignment to constant variable.这是最基础的规则:一旦用=赋值,就不能再改。
而且必须在声明时就赋值:
const name; // SyntaxError: Missing initializer in const declaration❗ 注意:const不等于“值不可变”
很多人误解const是“常量”,于是以为对象也不能改:
const user = { name: 'Alice' }; user.name = 'Bob'; // ✅ 合法! user.age = 25; // ✅ 也可以添加属性 user = {}; // ❌ 报错!不能重新赋值这里的关键在于:const保护的是“绑定”(binding),也就是变量指向的内存地址不能变,但对象本身的结构是可以修改的。
🧠 类比一下:
const像一把锁,锁住了门,但屋里的人可以自由活动。
如何真正冻结一个对象?
如果你希望对象也完全不可变,需要用Object.freeze():
const frozenUser = Object.freeze({ name: 'Alice' }); frozenUser.name = 'Bob'; // 无效(严格模式下会报错)不过注意:Object.freeze()是浅冻结,嵌套对象仍可变。如需深冻结,需要递归处理或使用库(如 Immutable.js)。
四、实际开发中的最佳实践
理论懂了,那在真实项目中该怎么用?
✅ 推荐原则:优先使用const,必要时降级为let,永远不用var
这是现代 JavaScript 社区的共识,也被 ESLint 等工具广泛推荐。
工作流程很简单:
1. 写变量时先敲const
2. 如果后续发现需要重新赋值(比如计数器、状态切换),再改成let
3. 绝对不要用var
这样做的好处:
- 提高代码可读性:一眼看出哪些变量是“稳定”的
- 减少意外修改风险
- 支持静态分析和打包优化(如 Webpack 的 Tree Shaking)
✅ 结合解构赋值,提升表达力
const { data, loading } = response; const [first, second] = list;这种写法清晰、简洁,配合const使用非常自然。
✅ 在 React 中的应用
const UserProfile = ({ user }) => { const [editing, setEditing] = useState(false); const displayName = user.nickname || user.name; return ( <div> <h1>{displayName}</h1> <button onClick={() => setEditing(true)}>Edit</button> </div> ); };这里几乎所有变量都用const,只有状态用useState管理。这正是函数式编程的思想体现:数据流明确,副作用可控。
五、常见误区与调试建议
❌ 误区1:const就是“常量”,所以性能更好?
No。const并不会带来运行时性能提升。它的主要价值是语义清晰 + 编译期检查。V8 引擎确实会对const做一些优化假设,但这不是你选择它的理由。
❌ 误区2:let可以重复声明?
不行!除非在不同作用域:
let a = 1; { let a = 2; // ✅ 不冲突,不同作用域 }但如果在同一层:
let b = 1; let b = 2; // ❌ 报错🛠️ 调试技巧:遇到ReferenceError怎么办?
典型错误信息:
ReferenceError: Cannot access 'xxx' before initialization说明你在let/const声明前就用了它。解决方法:
- 检查变量是否提前使用
- 看是否有条件声明导致逻辑错乱
- 使用浏览器 DevTools 查看调用栈
六、总结:这不是语法升级,是思维转变
let和const的引入,标志着 JavaScript 开发者思维方式的进化:
| 对比项 | var | let/const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 提升行为 | 提升且初始化为undefined | 提升但进入暂时性死区(TDZ) |
| 重复声明 | 允许 | 禁止 |
| 默认使用建议 | 已淘汰 | const优先,let次之 |
更重要的是,const所倡导的“不可变性”理念,正在深刻影响整个前端生态——从 Redux 的 state 设计,到 React 的纯组件思想,再到 Immer 这样的状态更新库,背后都是同一套哲学:减少副作用,增强可预测性。
所以,掌握let和const,不只是学会两个关键字,更是迈入现代 JavaScript 开发的第一步。
如果你还在用var,不妨从下一个变量开始,试试const——也许你会发现,代码突然变得更“干净”了。
👇 你在项目中是如何使用
let和const的?有没有因为var踩过坑?欢迎在评论区分享你的经历!