能用 let 别用 var

954 阅读2分钟

都 2021 年了,如果你的 JavaScript 代码中还有 var 的话,不妨尽快迁移到 let 或 const,这是 ES6 提供的全新的关键字,比 var 强的地方主要有以下两点:

  1. 避免变量提升(创建暂时性死区)
  2. 支持块级作用域(避免全局变量污染)

避免变量提升

变量提升是 JavaScript 程序员心中无法抹去的痛,例如:

console.log(a)
var a = 1

竟然会输出 undefined,上面的代码在几乎其他任何主流语言中都会报错,因为上学的时候,老师教过我们要「先声明后使用」,哪有变量在声明之前就能使用的说法?

是的,JavaScript 就是这么神奇,搞了一个「变量提升」的概念,也就是说上面的代码等价于:

var a
console.log(a)
a = 1

var 的这个特性会在不知不觉中给开发者带来难以察觉的问题,例如下面的代码:

var name = 'David'
function test() {
  // ... 这里是大量业务代码
  // 突然想访问全局变量 name
  console.log(name)
  // ... 又是大量业务代码
  // 不小心又定义了一个临时变量也叫 name
  var name = 'Lucy'
}

test()

中间的 name 是 undefined,你会百思不得其解,而改成 let 之后一切就都正常啦,因为你会得到一个错误:

ReferenceError: Cannot access 'name' before initialization

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

暂时性死区的本质就是:只要一进入当前作用域,所有变量就已经存在,但不能用,只有声明之后才能用。

支持块级作用域

ES5 中只有全局作用于和函数作用域,单纯大括号包裹的范围是不存在作用域的,例如:

{
  let a = 1
  var b = 2
}

console.log(a) // ReferenceError: a is not defined.
console.log(b) // 2

可以看到,b 直接暴露到全局,而 a 在花括号外面是访问不了的,尤其是在 for 循环当中,假如我们用 var 来声明变量,那么就会污染全局,而如果使用 let 则只对 for 循环内生效:

for (var j = 0; j < 10; j++) {
  // ...
}
console.log(j) // 10


for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i) // ReferenceError: i is not defined

当用 let 声明循环变量的时候,你是不是认为 i 的作用域就是花括号包裹的循环体?其实并不是的,因为 i 并没有在循环体内定义,其实 i 的作用域就是设置循环变量的那部分,即 for 语句,循环体内部其实是一个单独的子作用域,不信你看:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上面代码并未报错,而是输出了 3 次 abc,这表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域(因为同一个作用域不可使用 let 重复声明同一个变量)。当 for 循环中有异步操作的时候,用 varlet 的区别十分明显,例如:

for (var j = 0; j < 10; j++) {
  setTimeout(() => console.log(j), 100) // 打印 10 个 10
}

for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 100) // 打印 0 到 9
}