从var到let和const:现代 JavaScript 变量声明的彻底重构
你有没有在调试时遇到过这样的困惑:明明还没定义一个变量,却能提前访问到undefined?或者写了个循环,本想每次输出不同的索引值,结果异步回调里全变成了最后一个?
这些问题,曾是无数 JavaScript 开发者心头的痛。而这一切,在 ES6 引入let和const后,迎来了根本性的改变。
曾经的混乱:var的三大“坑”
在深入let和const之前,我们先回顾一下为什么它们如此重要——因为var真的太容易“坑人”了。
坑一:变量提升(Hoisting)带来的误解
console.log(name); // 输出:undefined var name = "前端工程师";这段代码不会报错,而是输出undefined。这是因为 JavaScript 引擎会把var声明“提升”到作用域顶部:
// 实际等价于 var name; console.log(name); // undefined name = "前端工程师";这种行为违背直觉:你能访问一个还没声明的变量。
坑二:函数作用域 vs 块级作用域
if (true) { var secret = "我是全局可见的!"; } console.log(secret); // 输出:"我是全局可见的!"尽管secret是在{}内部声明的,但它仍然可以在外部访问。也就是说,var只有函数作用域,没有块级作用域。
坑三:闭包陷阱的经典案例
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:3, 3, 3 (不是预期的 0, 1, 2)所有回调都共享同一个i,等到执行时,i已经变成 3 了。
let:让变量回归应有的生命周期
ES6 的let正是为了终结这些混乱而生。
它解决了什么?
- ✅真正的块级作用域:只在
{}内有效。 - ✅不存在变量提升:不能在声明前使用。
- ✅每次循环创建新绑定:完美解决闭包问题。
举个例子:用let修复上面那个恼人的循环
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出:0, 1, 2 —— 终于对了!为什么这次就对了?
因为let在每次迭代时都会创建一个新的词法环境,并将当前的i绑定进去。每个setTimeout捕获的是属于自己的那一份i。
💡 小知识:这背后的机制叫做“迭代作用域绑定”(per-iteration binding),是 V8 引擎专门为
let实现的优化。
不允许重复声明
let count = 10; let count = 20; // SyntaxError: Identifier 'count' has already been declared这个限制大大减少了命名冲突的风险。
暂时性死区(TDZ):安全的“未初始化状态”
console.log(age); // ReferenceError: Cannot access 'age' before initialization let age = 25;与var的“提升为 undefined”不同,let变量从进入作用域到声明语句被执行之间,处于“暂时性死区”。此时访问它会直接抛错,而不是返回undefined。
这其实是一种进步:宁可报错,也不要让你误以为变量存在但值不对。
const:不只是“常量”,更是一种编程哲学
如果说let是对var的修正,那const就是一次思想升级。
最基本规则:声明即赋值,不可重赋
const PI = 3.14159; PI = 3.14; // TypeError: Assignment to constant variable.注意关键点:const阻止的是“重新赋值”(re-assignment),不是“修改内容”。
对象和数组怎么办?
const user = { name: "Alice" }; user.name = "Bob"; // ✅ 允许 user.age = 28; // ✅ 允许 // user = {}; // ❌ 报错:不能重新赋值 const colors = ['red']; colors.push('blue'); // ✅ 允许 // colors = ['green']; // ❌ 报错所以const并不等于“完全不可变”。它只是锁住了变量名和内存地址之间的绑定关系。
如何实现真正不可变的对象?
如果你确实需要冻结整个对象结构,可以结合Object.freeze():
const config = Object.freeze({ apiBase: 'https://api.example.com', timeout: 5000, retries: 3 }); // config.apiBase = 'xxx'; // 在严格模式下会静默失败或抛错⚠️ 注意:
Object.freeze()是浅冻结。如果对象嵌套很深,还需要递归处理才能实现深冻结。
实战中的最佳实践:我们应该怎么选?
面对let和const,很多新手会纠结:“到底该用哪个?” 其实答案很简单:
默认用
const,只有当你明确需要重新赋值时才用let
这是 Airbnb、Google 等主流编码规范的一致建议,也被 ESLint 的prefer-const规则强制推行。
一张表帮你快速决策
| 场景 | 推荐关键字 | 示例 |
|---|---|---|
| API 地址、配置项 | const | const BASE_URL = '/api/v1'; |
| 组件主题、枚举值 | const | const THEME = { primary: '#007bff' }; |
| 模块导出对象 | const | export const actions = { ... }; |
| 循环计数器 | let | for (let i = 0; i < len; i++) |
| 条件分支中需变更的变量 | let或多个const | 视情况而定 |
更优雅的做法:优先使用多个const而非可变的let
与其这样做:
let status; if (user.loggedIn) { status = 'active'; } else { status = 'guest'; }不如拆成两个const:
const status = user.loggedIn ? 'active' : 'guest';这样不仅代码更简洁,也避免了中间状态的存在,逻辑更清晰。
在框架开发中如何应用?
以 React 函数组件为例:
import { useEffect, useState } from 'react'; function ProductList({ categoryId }) { // ✅ 固定资源路径 → const const API_ENDPOINT = `https://api.store.com/categories/${categoryId}/products`; // ✅ 状态交给 Hook 管理 → const(但本质是可变) const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); // ✅ 临时计算结果 → const const totalItems = products.length; useEffect(() => { setLoading(true); // ✅ 局部临时变量 → let(仅用于同步流程) let fetchedData = null; fetch(API_ENDPOINT) .then(res => res.json()) .then(data => { fetchedData = data; // 修改引用内部内容 setProducts(fetchedData); setLoading(false); }); }, [categoryId]); return ( <div> {loading ? <p>加载中...</p> : <p>共 {totalItems} 件商品</p>} </div> ); }可以看到:
- 所有“静态”信息都用const
- 状态管理交给专门的机制(如useState)
- 即使用了let,也只是短暂存在于同步逻辑中
性能与工程化优势:不只是语法糖
很多人以为let/const只是语法改进,其实它们还带来了实实在在的工程收益。
1. 更利于静态分析与压缩
现代打包工具(如 Webpack + Terser)可以通过分析const声明进行:
- 常量折叠(Constant Folding)
- 死代码消除(Dead Code Elimination)
- 更激进的变量内联
例如:
const DEBUG = false; if (DEBUG) { console.log("调试信息"); // 这段代码可能被完全移除 }2. 支持 Tree Shaking
模块化的const导出更容易被识别为无副作用,从而支持更精准的 Tree Shaking,减小最终包体积。
3. 提升团队协作效率
当你看到某个变量是const,你就知道它在整个作用域内不会被篡改。这对阅读代码的人来说是一个强有力的信号:“放心看,这个值不会变”。
写在最后:从“能改”到“不该改”的思维转变
掌握let和const的意义,远不止学会两个新关键字那么简单。
它代表了一种编程范式的演进:
- 从随意修改变量到最小权限原则
- 从依赖运行时调试到通过语法约束预防错误
- 从动态灵活到可预测、可维护
如今,几乎所有现代项目都已经启用 ES6+ 语法。TypeScript 更是进一步强化了这种不可变的思想。可以说,const正在成为新时代的默认选择。
所以,下次你敲下变量声明时,不妨先问自己一句:
“这个变量,真的需要被重新赋值吗?”
如果答案是否定的,那就果断使用const—— 让你的代码从第一行就开始变得更健壮。