每种编程语言都有循环。循环执行某个操作(也就是一段工作)多次,通常是对数组或列表中的每一项进行操作,或者重复执行直到满足某个条件为止。
JavaScript 尤其提供了多种不同类型的循环。我自己甚至都没有用过所有这些类型,出于好奇心,我想做个高层次的概览。结果发现,至少有几种循环类型没被广泛采用是有充分理由的。
那么,咱们就先用 for
循环开始,花点 while
时间来探索各种不同的循环类型,看看我们能用 each
做些什么,以及为什么你可能会选择一种而非另一种。读到最后你会觉得我这里的小文字游戏绝对逗乐。
while
和 do...while
循环
首先登场的是 while
循环。这是最基础的循环类型,在很多情况下也是最容易阅读和最快的。它通常用于做某件事直到满足某个条件。这也是最容易制造无限循环或永不停止的循环的方式。还有 do...while
语句。实际上,唯一的区别在于条件是在每次迭代的末尾而不是开头检查。
// 从数组中移除第一个元素并打印出来,直到数组为空
let queue1 = ["a", "b", "c"];
while (queue1.length) {
let item = queue1.shift();
console.log(item);
}
// 上面的相同逻辑,但也会打印数组为空时的信息
let queue2 = [];
do {
let item = queue2.shift() ?? "empty";
console.log(item);
} while (queue2.length);
for
循环
接下来是 for
循环。如果你需要重复执行某个操作一定次数,比如十次,那这就是首选。这种循环对编程新手来说可能有点吓人,但是把相同的循环用 while
风格重写一下,可以帮助理解语法,也更容易记住。
// 打印数字 1 到 5
for (let i = 1; i <= 5; i++) {
console.log(i);
}
// 同样的事情,但是用 `while` 循环实现
let i = 1; // `for` 循环的第一部分
// 第二部分
while (i <= 5) {
console.log(i);
i++; // 第三部分
}
("end");
for...of
和 for await...of
循环
for...of
循环是最简单的方式来遍历数组。
let myList = ["a", "b", "c"];
for (let item of myList) {
console.log(item);
}
不过它们不仅仅限于数组。技术上讲,它们可以遍历实现了所谓的 可迭代协议 的任何东西。有一些内置类型实现了该协议:数组、映射、集合和字符串,这些是最常见的,但你也可以在自己的代码中实现该协议。做法是在任意对象中添加一个 [Symbol.iterator]
方法,而这个方法应该返回一个迭代器。这有点让人困惑,但大概意思是可迭代的对象是一种特殊的方法,返回迭代器;可以说是迭代器的工厂方法。一种特殊的函数叫做 生成器,它既能返回可迭代对象也能返回迭代器。
let myList = {
*[Symbol.iterator]() {
yield "a";
yield "b";
yield "c";
},
};
for (let item of myList) {
console.log(item);
}
上面提到的所有东西都有异步版本:异步可迭代对象、异步迭代器和异步生成器。你可以用 for await...of
来使用异步可迭代对象。
async function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// 这次我们不是创建一个可迭代对象,而是生成器
async function* aNumberAMinute() {
let i = 0;
while (true) {
// an infinite loop
yield i++;
// pause a minute
await delay(60_000);
}
}
// 它是一个生成器,所以我们需要自己调用它
for await (let i of aNumberAMinute()) {
console.log(i);
// stop after one hour
if (i >= 59) {
break;
}
}
有一点不太明显的是,你可以用非异步可迭代对象与 for await...of
一起使用,而且完全没问题。然而反过来就不行;你不能用异步可迭代对象与 for...of
一起使用。
forEach
和 map
“循环”
虽然这些严格意义上不算循环,但你可以用它们来遍历列表。
说说 forEach
方法吧。历史上它的速度远不如 for
循环。我认为现在有些情况下这种情况可能已经改变,但如果性能是个问题,我还是会避免使用它。有了 for...of
之后,我不确定还有什么理由要用它。也许唯一的原因是你已经有了一个可以直接作为回调使用的函数,但你也可以很容易地在 for...of
的主体里调用同一个函数。
forEach
还接收每个元素的索引,所以这可能是你需要的功能。最终是否使用它,可能会取决于你正在处理的其他代码是否也在使用它,但如果是新写的代码我个人会尽量避免使用它。
let myList = ["a", "b", "c"];
for (let item of myList) {
console.log(item);
}
// 如果我需要索引,或许可以用 `forEach`
["a", "b", "c"].forEach((item, index) => {
console.log(`${index}: ${item}`);
});
同时,map
实质上是将一个数组转换为另一个数组。它与 forEach
一样有性能影响,但它比替代方案更好读一点。当然这是主观判断,就像 forEach
一样,你会希望遵循你的其他代码的做法。在 React 和受 React 启发的库中,你会经常看到它作为遍历数组并在 JSX 中输出项目列表的主要方式。
function MyList({items}) {
return (
<ul>
{items.map((item) => {
return <li>{item}</li>;
})}
</ul>
);
}
for...in
循环
说到 JavaScript 中的循环,如果不提 for...in
语句,这个列表就不完整了,因为它可以遍历对象的字段。它还会遍历通过对象原型链继承的字段,这正是我一直避免使用它的原因。
话虽如此,如果你有一个对象字面量,那么 for...in
可能是遍历该对象键的有效方式。值得一提的是,如果你编写 JavaScript 已经有一段时间了,你可能还记得键的顺序曾经在不同浏览器之间不一致,但现在顺序是一致的。任何可能作为数组索引的键(即正整数)都会按升序排列在前面,然后是按声明顺序排列的其他键。
let myObject = {
a: 1,
b: 2,
c: 3,
};
for (let k in myObject) {
console.log(myObject[k]);
}
结语
循环是许多程序员每天都在使用的东西,尽管我们可能会习以为常,不太去思考它们。
但当你退一步来看我们在 JavaScript 中遍历事物的各种方式时,你会发现其实有很多种方法可以做到这一点。不仅如此,它们之间存在显著——即使细微——的区别,这些差异确实会影响你的编写方式。