在 JavaScript 的演进史上,2015 年发布的 ES6(ECMAScript 2015)无疑是一个里程碑。它不仅引入了类、模块和箭头函数,更彻底重塑了我们声明变量的方式——let 和 const 的诞生。
直到今天,许多开发者依然在面对 var、let、const 时感到困惑:到底该用哪个?为什么官方推荐不再使用 var?
本文将带你深入理解这三者的核心差异,通过实战场景剖析,帮你建立一套现代化的变量声明思维模型。
var 的“原罪”在 ES6 之前,var 是我们声明变量的唯一选择。但它有两个著名的“设计缺陷”,常常导致难以排查的 Bug:
var 只有函数作用域,没有块级作用域。这意味着在 if、for 等代码块中声明的变量,会“泄露”到整个函数甚至全局作用域中。
function getData() {
if (true) {
var data = "敏感数据";
}
// data 在这里依然可以访问!这往往不是我们想要的
console.log(data);
}
var 声明的变量会被提升到作用域顶部,但初始化不会。这导致你在声明之前访问变量,得到的不是报错,而是神秘的 undefined。
console.log(user); // 输出:undefined (而不是报错)
var user = "Alice";
这种“静默失败”是无数逻辑错误的温床。
为了解决上述问题,let 和 const 应运而生。它们不仅修复了作用域问题,还引入了暂时性死区(TDZ),让错误尽早暴露。
表格
| 特性 | var(旧时代) |
let(现代可变) |
const(现代常量) |
|---|---|---|---|
| 作用域 | 函数级 | 块级 ({}) |
块级 ({}) |
| 变量提升 | ✅ 有 (初始化为 undefined) | ❌ 无 (存在暂时性死区) | ❌ 无 (存在暂时性死区) |
| 重复声明 | ✅ 允许 | ❌ 禁止 | ❌ 禁止 |
| 重新赋值 | ✅ 允许 | ✅ 允许 | ❌ 禁止 |
| 必须初始化 | 否 | 否 | 是 |
| 全局对象属性 | 是 (挂载到 window) | 否 | 否 |
💡 关键概念:暂时性死区 (TDZ)
在使用let或const声明变量之前,任何对该变量的访问都会直接抛出ReferenceError。这强制你“先声明,后使用”,从根源上杜绝了隐式错误。
这是面试中最常考的场景,也是 var 最致命的弱点。
❌ 错误示范 (var):
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// 输出:5, 5, 5, 5, 5
// 原因:i 是函数作用域,所有定时器共享同一个 i,循环结束时 i 变成了 5
✅ 正确示范 (let):
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// 输出:0, 1, 2, 3, 4
// 原因:let 拥有块级作用域,每次循环都会创建一个新的 i 绑定
const 真的是“常量”吗?很多新手误以为 const 定义的内容完全不可变,这是一个巨大的误区。
const 保证的是“引用地址”不变,而不是“内容”不变。
const user = { name: "Alice", age: 25 };
// ✅ 允许:修改对象内部的属性
user.age = 26;
user.name = "Bob";
// ❌ 禁止:重新赋值给新的对象
user = { name: "Charlie" }; // TypeError: Assignment to constant variable.
🚀 进阶技巧:如何真正冻结对象?
如果你希望对象连属性都不能修改,需要配合 Object.freeze():
const config = Object.freeze({
apiVersion: "v1",
debug: false
});
config.debug = true; // 严格模式下报错,非严格模式下静默失败
在现代开发中,我们不再纠结“能不能用”,而是遵循一套最佳实践优先级。
const,按需 let,拒绝 var场景 1:配置项与工具函数
首选 const。配置、API 地址、数学常数等一旦定义就不应改变的数据。
const API_BASE_URL = "https://api.example.com";
const MAX_RETRY_COUNT = 3;
const formatDate = (date) => { ... };
场景 2:状态管理与循环计数器
使用 let。当你知道这个变量在未来需要被重新赋值时(如累加器、开关状态、循环索引)。
let isLoading = false;
let currentIndex = 0;
for (let i = 0; i < items.length; i++) {
// 处理逻辑
}
场景 3:维护老旧代码
仅在必要时保留 var。如果你在维护一个必须兼容 IE9 以下浏览器的古老项目,或者在处理一些依赖 var 提升特性的遗留逻辑(极少见),才考虑使用 var。否则,请果断重构。
忘记写声明关键字是新手常犯的错误。在非严格模式下,直接赋值会创建全局变量,污染 window 对象。
// ❌ 危险
function test() {
count = 10; // 隐式创建了全局变量 window.count
}
// ✅ 安全
function test() {
let count = 10; // 局部变量
}
不要依赖人工检查,让工具帮你守住底线。在 .eslintrc 中配置:
{
"rules": {
"no-var": "error", // 禁止使用 var
"prefer-const": "error", // 如果变量未被重新赋值,强制使用 const
"no-unused-vars": "warn" // 警告未使用的变量
}
}
开启 prefer-const 规则后,如果你写了 let x = 1 但后续没改过 x,ESLint 会提示你改为 const,这能极大提升代码的可读性和安全性。
从 var 到 let/const 的转变,不仅仅是语法的更新,更是编程思维的升级:
const 告诉阅读代码的人,“这个数据不应该被改变”,降低了心智负担。记住这句口诀:
默认全用
const,非要改动才let,除非万不得已,否则别碰var。
当你开始习惯这种写法,你会发现你的代码变得更加健壮、易读,且充满了现代工程化的美感。
本文被 前端开发 专题收录