ES6 let 和 const
ES5 时代只有 var 一种声明方式,它带来的作用域混乱问题困扰了 JS 开发者十几年。
ES6 新增了 let 和 const,从根上解决了这些问题,也是 ES6 中最基础的语法。
var 的三大历史遗留问题
在认识新语法前,先看看 var 到底哪里不好。
1. 没有块级作用域
var 只有函数作用域和全局作用域,没有块级作用域。这意味着 if、for、while 的大括号 {} 挡不住它:
// 示例
if (true) {
var a = 1; // var 不认这个 if 大括号
}
console.log(a); // 1 —— 变量"泄漏"到了 if 外面
for (var i = 0; i < 3; i++) {
// 循环体
}
console.log(i); // 3 —— i 也没有被关在 for 里
这与大多数语言(Java、C++)的直觉相悖:块里声明的东西应该留在块里。
var 让你很容易写出”悄悄串改状态”的 bug。
2. 变量提升造成”先使用后声明”的错觉
var 声明的变量会被提升到作用域顶部,只是值还是 undefined:
// 示例
console.log(x); // undefined —— 不报错,但结果莫名其妙
var x = 10;
初学者很容易因此写出”看起来能跑、实际是坑”的代码。更糟的是用 var 重复声明同名变量,还完全合法:
// 示例
var n = 1;
var n = 2; // 不报错,n 被静默覆盖
3. 循环异步的经典坑
配合闭包和异步,var 的坑更明显:
// 示例
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 3、3、3
}, 100);
}
var 把同一个 i 暴露给三个回调,循环结束后 i 已经是 3,所以三个回调读到的都是 3。
let/const 为每一次循环单独绑定一份变量,天然规避了这个坑(见下文)。
认识 let 和 const
ES6 新增的两种声明方式:
let count = 1; // 可重新赋值
const PI = 3.14; // 一旦赋值,不能再改(对象除外,见下文)
块级作用域
let/const 的变量只在大括号内生效,出了块就访问不到:
// 示例
if (true) {
let a = 1;
const b = 2;
}
console.log(a); // 报错:a 未定义
console.log(b); // 报错:b 未定义
这也是前面 var 的第一个坑:let/const 作用域从严,超出大括号即失效。
暂时性死区(TDZ)
let/const 声明的变量,在声明之前的区域称为暂时性死区,访问会直接报错,而不是像 var 得到 undefined:
// 示例
console.log(x); // 报错:ReferenceError
let x = 10;
这其实是”把问题提早暴露”,避免带着 undefined 跑到运行时才悄悄出错。
用 let 还是 const
记住一句口诀就能应付 90% 场景:能 const 就 const,不能 const 才 let,绝不使用 var。
| 情况 | 用谁 |
|---|---|
| 变量声明后将来不会再重新赋值 | const |
| 变量需要反复重新赋值 | let |
| 特殊情况(老代码、必须用 var) | var |
有一个问题,新手很容易误解,const 究竟”不能改”什么?
其实,const 禁止重新赋值(更改变量指向),但允许修改对象/数组的内部内容。
// 示例
const user = { name: '小明' };
user.name = '小红'; // 可以:修改对象内部属性
// user = {}; // 报错:重新赋值不能
const arr = [1, 2, 3];
arr.push(4); // 可以:往数组里加内容
arr.length = 2; // 也可以
// arr = []; // 报错:重新指向不允许
直观理解:const 是铁打的变量引用,流水的具体内容——你不能再让它”换人了”,但可以让它指向的那个对象/数组自己变。
解决 var 的三个坑
对照上一节,let/const 如何逐个解决:
1 块级作用域:if/for 里声明的 let/const,在大括号外访问报”未定义”,变量不再泄漏。
2 谜之提升:let/const 声明前使用会直接报错(TDZ),杜绝”先用后声明”的静默问题;重复声明同名变量也会直接报错:
// 示例
let n = 1;
let n = 2; // 报错:Cannot redeclare block-scoped variable
3 循环闭包坑:这是新手最难理解、面试最常见的一个坑。先看问题版(用 var):
// 示例
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 3、3、3
}, 100);
}
为什么会输出三个 3,而不是 0、1、2?拆成两步就懂了:
setTimeout是”延迟执行”:setTimeout(callback, 100)的意思是”100 毫秒之后再执行 callback”,浏览器不会立刻执行,而是先把它记下来,等循环全部跑完才轮到它们。- 闭包捕获的是”变量 i”,不是”当时的 i 值”:三个回调函数都引用同一个
var i。循环走完后,i已经等于 3(不满足i < 3才停),此时 100 毫秒到了,三个回调才依次执行,读到的自然全是 3。
这里的 function () { console.log(i) } 就叫闭包——一个”记住了外部变量的函数”。因为记的是变量本身(而不是那一刻的数字),所以变量后来的值一变,闭包看到的也跟着变。
换成 let 就正常了:
// 示例
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 0、1、2(正确!)
}, 100);
}
原因在于 let 的块级作用域:for 每循环一次,都会为这一轮的 i 单独创建一份新变量,闭包捕获的是”这一轮专属的 i“。三轮分别捕获 0、1、2 三个互不干扰的变量,等延迟结束后各自输出自己的那份,自然就正确了。
一句话总结:var 让所有闭包共享同一个 i(最后都读到 3);let 让每轮闭包各拿各的 i(0、1、2 各归各)。