六维教程

ES6 let 和 const

ES5 时代只有 var 一种声明方式,它带来的作用域混乱问题困扰了 JS 开发者十几年。

ES6 新增了 letconst,从根上解决了这些问题,也是 ES6 中最基础的语法。

var 的三大历史遗留问题

在认识新语法前,先看看 var 到底哪里不好。

1. 没有块级作用域

var 只有函数作用域全局作用域,没有块级作用域。这意味着 ifforwhile 的大括号 {} 挡不住它:

// 示例
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 为每一次循环单独绑定一份变量,天然规避了这个坑(见下文)。

认识 letconst

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% 场景:constconst,不能 constlet,绝不使用 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?拆成两步就懂了:

  1. setTimeout 是”延迟执行”setTimeout(callback, 100) 的意思是”100 毫秒之后再执行 callback”,浏览器不会立刻执行,而是先把它记下来,等循环全部跑完才轮到它们。
  2. 闭包捕获的是”变量 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 各归各)。

上一篇
ES6 概述
下一篇
ES6 箭头函数