【原理】你真的了解let/const吗

4/5/2021 原理ES6

# 原理

今天看书看到下面几句话:

《深入理解ES6》 (opens new window) 第一章,块级作用域绑定,第9页循环中的let声明一节中提到

每次循环迭代都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。

《JavaScript高级程序设计(第4版)》 (opens new window) 第三章,语言基础,3.3.2节,第28页中也提到

在使用let声明迭代变量时,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。

最佳实践

  1. for循环中推荐使用let来声明迭代变量
  2. for...infor...of推荐使用const来声明迭代变量。

光看有点不好理解为什么这么设计,写点代码来实践一下。

# 代码实践

# 1. 异步打印

for (var i = 0; i < 3; ++i) {
  setTimeout(() => {console.log(i)})
}
1
2
3
查看结果

3 3 3

原因:由于var声明会进行变量提升。函数指向全局作用域的i,打印i的时候,i的值已经变成3了。

改成let试试

for (let i = 0; i < 3; ++i) {
  setTimeout(() => {console.log(i)})
}
1
2
3
查看结果

0 1 2

原因:每次循环迭代都会在其块级作用域中创建一个新变量i

异曲同工的ES5写法

for (var i = 0; i < 3; ++i) {
  (function (i){
    setTimeout(() => {console.log(i)})
  }(i))
}
1
2
3
4
5
查看结果

0 1 2

原因:利用闭包,人为做了一次i的拷贝。

# 2. 在循环中创建函数

var fns = []
for (var i = 0; i < 3; ++i) {
  fns.push(() => { console.log(i) })
}
fns.forEach(fn => fn())
1
2
3
4
5
查看结果

3 3 3

原因:由于var声明会进行变量提升。函数使用全局作用域的同一个i,打印i的时候,i的值已经变成3了。

改成let试试

var fns = []
for (let i = 0; i < 3; ++i) {
  fns.push(() => { console.log(i) })
}
fns.forEach(fn => fn())
1
2
3
4
5
查看结果

1 2 3

原因:let每轮循环都创建了一个新的变量i,三个函数指向了三个不同的i

异曲同工的ES5写法

var fns = []
for (var i = 0; i < 3; ++i) {
  (function (i) {
    fns.push(() => { console.log(i) })
  }(i))
}
fns.forEach(fn => fn())
1
2
3
4
5
6
7
查看结果

0 1 2

原因:利用函数作用域的特点,每次循环都人为拷贝了变量i

# 使用引用类型变量进行循环

for (const i = {a: 0}; i.a < 3; i.a = i.a + 1) {
  i[`${i.a}`] = i.a
  setTimeout(() => console.log(i), 1000 * i.a)
}
1
2
3
4
查看结果

{ '0': 0, '1': 1, '2': 2, a: 3 }

{ '0': 0, '1': 1, '2': 2, a: 3 }

{ '0': 0, '1': 1, '2': 2, a: 3 }

原因:每轮循环中,JavaScript引擎拷贝的i实际上是一个指向堆内存对象的地址。

因为这里只是改变了堆内存对象属性的值,并没有改变i的值(实际上i是一个指向堆内存对象的指针),可以使用const来声明i,这与使用letvar结果相同。

如果JavaScript提供了查看变量栈内存地址的API,那么验证这种说法就轻而易举了。

Last Updated: 4/8/2021, 12:53:45 PM