JS `for…of` 循环与 `let` 的结合:每次迭代创建新的变量绑定

各位观众,欢迎来到今天的“JavaScript冷知识小课堂”。今天我们要聊聊一个在 for...of 循环中,let 默默施展的小魔法:每次迭代都创建新的变量绑定。听起来有点拗口,但保证你听完之后,会觉得“哇,原来你是这样的 for...of!”

开场白:var 的那些年,我们一起踩过的坑

在深入 for...oflet 的美妙结合之前,我们先简单回顾一下 var 的“辉煌”历史。在 ES6 之前,var 是我们在 JavaScript 中声明变量的主要方式。但是,var 有一个让人头疼的特点:函数作用域。这意味着,如果你在循环中使用 var 声明变量,那么这个变量实际上只有一个,每次循环都会改变它的值。

来看个例子:

function demonstrateVarProblem() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(i);
    }, 1000);
  }
}

demonstrateVarProblem(); // 输出: 5 5 5 5 5

这段代码,本意是想在 1 秒后依次输出 0, 1, 2, 3, 4。但实际运行结果却是输出了五次 5。 这是因为 var i 在循环外部声明,所有 setTimeout 中的函数都共享同一个 i 变量。当循环结束后,i 的值变成了 5,所以所有函数都输出了 5。

为了解决这个问题,我们通常需要使用闭包来“捕获”每次循环的 i 值。

function demonstrateVarSolution() {
  for (var i = 0; i < 5; i++) {
    (function(j) { // 使用立即执行函数表达式 (IIFE) 创建闭包
      setTimeout(function() {
        console.log(j);
      }, 1000);
    })(i);
  }
}

demonstrateVarSolution(); // 输出: 0 1 2 3 4

虽然解决了问题,但是代码看起来有点啰嗦,不够优雅。 这时候,let 就闪亮登场了。

主角登场:let 的“块级作用域”大法

let 关键字引入了块级作用域的概念。这意味着,let 声明的变量只在声明它的块(通常是 {} 包裹的代码块)中有效。 这就像给每个循环都创建了一个小房间,let 声明的变量就只属于这个小房间,不会受到其他房间的影响。

再来看看 for 循环中使用 let 的例子:

function demonstrateLetProblem() {
  for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(i);
    }, 1000);
  }
}

demonstrateLetProblem(); // 输出: 0 1 2 3 4

这次,我们只需要把 var 换成 let,就能得到正确的结果。 let 每次循环都会创建一个新的 i 变量,每个 setTimeout 中的函数都访问的是自己循环的 i 值。

for...of 循环:更优雅的选择

for...of 循环是 ES6 引入的另一种循环方式,专门用于迭代可迭代对象(例如数组、字符串、Map、Set 等)。 它比传统的 for 循环更加简洁易懂,也更不容易出错。

const myArray = [10, 20, 30, 40, 50];

for (const element of myArray) {
  console.log(element); // 输出: 10 20 30 40 50
}

for...of + let:天作之合

现在,我们把 for...of 循环和 let 结合起来,看看会发生什么神奇的事情。

const myNumbers = [1, 2, 3, 4, 5];

for (const number of myNumbers) {
  let squaredNumber = number * number;
  setTimeout(function() {
    console.log(`The square of ${number} is ${squaredNumber}`);
  }, number * 100); // 延迟时间不同,方便观察
}

这段代码会依次输出:

The square of 1 is 1 (100ms 后)
The square of 2 is 4 (200ms 后)
The square of 3 is 9 (300ms 后)
The square of 4 is 16 (400ms 后)
The square of 5 is 25 (500ms 后)

注意,即使 setTimeout 是异步的,每个回调函数都正确地访问到了自己循环中的 numbersquaredNumber 的值。 这是因为 letfor...of 循环的每次迭代中,都会创建一个新的 squaredNumber 变量绑定。

深入解析:每次迭代创建一个新的变量绑定

for...of 循环与 let 结合的魔力在于,每次迭代都会创建一个新的变量绑定。 这意味着,每次循环都会分配一块新的内存空间来存储 let 声明的变量。 每个回调函数都“记住”了自己创建时的变量绑定,所以可以正确地访问到对应的值。

可以用一个表格来概括 varletfor 循环和 for...of 循环中的行为:

循环类型 变量声明 作用域 每次迭代创建新的绑定
for var 函数作用域
for let 块级作用域
for...of var 函数作用域
for...of let 块级作用域

const 的“只读”特性与 for...of

const 关键字用于声明常量,这意味着它的值在声明后不能被修改。 虽然 const 也有块级作用域,但它在 for...of 循环中的行为略有不同。

const myStrings = ["apple", "banana", "cherry"];

for (const str of myStrings) {
  console.log(str); // 输出: apple banana cherry
  // str = "orange"; // TypeError: Assignment to constant variable.
}

在上面的例子中,str 被声明为 const,所以你不能在循环体内修改它的值。 但是,这并不妨碍 for...of 循环的正常运行。 const 只是保证了变量的值不能被重新赋值,但它仍然会在每次迭代中创建一个新的绑定,只是这个绑定是只读的。

需要注意的点:for 循环的头部

在传统的 for 循环中,let 的行为稍微特殊一些。

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

虽然 i 是在 for 循环的头部用 let 声明的,但它仍然表现出每次迭代创建一个新的绑定的行为。 这是因为 JavaScript 引擎对 for 循环的头部做了特殊处理,使其表现得好像每次迭代都会重新声明 i

最佳实践:优先使用 const,其次 let

在现代 JavaScript 开发中,我们应该尽可能地使用 const 来声明变量,只有在变量需要被重新赋值时才使用 let。 这有助于提高代码的可读性和可维护性,也能减少出错的可能性。

for...of 循环中,如果循环变量不需要被修改,那么就应该使用 const 来声明它。

const colors = ["red", "green", "blue"];

for (const color of colors) {
  console.log(color.toUpperCase()); // 输出: RED GREEN BLUE
}

总结:for...of + let 的强大之处

for...of 循环与 let 的结合,为我们提供了一种简洁、安全、高效的迭代方式。 它避免了 var 的陷阱,让我们可以更加自信地编写异步代码。

  • let 声明的变量具有块级作用域。
  • for...of 循环的每次迭代都会创建一个新的变量绑定。
  • const 声明的常量也具有块级作用域,但其值不能被修改。
  • 优先使用 const,其次 let

彩蛋:for...in 循环的“坑”

虽然 for...in 循环也可以用来迭代对象,但是它有一些需要注意的地方。 for...in 循环会遍历对象的所有可枚举属性,包括从原型链上继承的属性。 这可能会导致一些意想不到的结果。

const myObject = { a: 1, b: 2, c: 3 };

for (const key in myObject) {
  console.log(key, myObject[key]); // 输出: a 1, b 2, c 3
}

为了避免遍历到原型链上的属性,我们可以使用 hasOwnProperty() 方法来过滤。

const myObject = { a: 1, b: 2, c: 3 };

for (const key in myObject) {
  if (myObject.hasOwnProperty(key)) {
    console.log(key, myObject[key]); // 输出: a 1, b 2, c 3
  }
}

总而言之,除非你有特殊的需求,否则应该优先使用 for...of 循环来迭代数组和字符串等可迭代对象。 for...in 循环更适合用于遍历对象的属性,但需要小心处理原型链上的属性。

今天的“JavaScript冷知识小课堂”就到这里。希望大家有所收获,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注