各位观众,欢迎来到今天的“JavaScript冷知识小课堂”。今天我们要聊聊一个在 for...of
循环中,let
默默施展的小魔法:每次迭代都创建新的变量绑定。听起来有点拗口,但保证你听完之后,会觉得“哇,原来你是这样的 for...of
!”
开场白:var
的那些年,我们一起踩过的坑
在深入 for...of
和 let
的美妙结合之前,我们先简单回顾一下 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
是异步的,每个回调函数都正确地访问到了自己循环中的 number
和 squaredNumber
的值。 这是因为 let
在 for...of
循环的每次迭代中,都会创建一个新的 squaredNumber
变量绑定。
深入解析:每次迭代创建一个新的变量绑定
for...of
循环与 let
结合的魔力在于,每次迭代都会创建一个新的变量绑定。 这意味着,每次循环都会分配一块新的内存空间来存储 let
声明的变量。 每个回调函数都“记住”了自己创建时的变量绑定,所以可以正确地访问到对应的值。
可以用一个表格来概括 var
、let
在 for
循环和 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冷知识小课堂”就到这里。希望大家有所收获,下次再见!