106.JavaScript中For循环的3种版本和使用场景

1.经典的 For 循环

首先来看经典的 For 循环;你可以在其中定义内部计数器,设置中断条件和步进更改(通常是增加或减少计数器计数)。
具体的语法为:
for ([计数器定义]; [条件中断定义]; [步进定义]) {
//……这里是重复的代码
}
以前你肯定写过这种代码,最常见的形式是:
for (let counter = 0; counter < 10; counter++) {
console.log(counter);
}
虽说这种代码可以运行得很好,但是 For 循环的各个部分要更灵活一些。实际上,你应该将它们视为:
for(
[EXPRESSION EXECUTED ONLY ONCE AT THE START OF THE LOOP(表达式仅在循环开始时执行一次)];
[BOOLEAN CONDITION CHECKED ON EVERY STEP(每一步都检查布尔条件)];
[EXPRESSION EXECUTED ON EVERY STEP OF THE LOOP(表达式在循环的每一步都执行)]
)

也就是说,你可以使用多个计数器进行 For 循环,或者在不一定影响计数器的情况下执行每一步的代码。这里举几个例子。例如,这是一个完全有效的循环:
for (let a = 0, b = 0; a < 10 && b < 100; a++, b += 10) {
console.log(a, b);
}
/
0 0
1 10
2 20
3 30
4 40
5 50
6 60
7 70
8 80
9 90
/
你甚至可以更进一步,从上面的一般用例中跳出来:
for (
let a = 0, b = 0;
a < 10 && b < 100;
console.log(“Your counters are at:”, ++a, (b += 2))
) {}
/
Your counters are at: 1 2
Your counters are at: 2 4
Your counters are at: 3 6
Your counters are at: 4 8
Your counters are at: 5 10
Your counters are at: 6 12
Your counters are at: 7 14
Your counters are at: 8 16
Your counters are at: 9 18
Your counters are at: 10 20
/
只要你记得该函数的返回值将被强制转换为布尔值,就可以用中间表达式代替函数调用。
function isItDone(a) {
console.log(“fn called!”);
return a < 10;
}
for (let a = 0; isItDone(a); a++) {
console.log(a);
}
/
fn called!
0
fn called!
1
fn called!
2
fn called!
3
fn called!
4
fn called!
5
fn called!
6
fn called!
7
fn called!
8
fn called!
9
fn called!
/
那么如何在经典的 For 循环中处理 异步代码 呢?多亏了我们的新朋友 async/await,这很容易:
const fs = require(“fs”);
async function read(fname) {
return new Promise((resolve, reject) => {
fs.readFile(fname, (err, content) => {
if (err) return reject(err);
resolve(content.toString());
});
});
}
(async () => {
let files = [“file1.json”, “file2.json”];
for (let i = 0; i < files.length; i++) {
let fcontent = await read(files[i]);
console.log(fcontent);
console.log(“———-“);
}
})();
注意,这里我们可以简单地使用循环,就好像背后没有异步机制一样。这就是 async/await,因为有它,我们才能重新依靠诸如 For 循环 之类的基本构造来迭代一组异步指令。过去,如果你想使用回调或 Promise 实现同一目标,则逻辑会复杂得多。这就是诸如 async.js 之类的库诞生的起源。
顺便说一句:在我的示例中,for 循环位于 IIFE内;你可能已经知道,这只是因为 await 指令需要位于异步函数内,否则 Node 是不会允许它的。

2.For…in 和 For…of

它们是上一版本的非常相似的变体,但它们并不是同一种循环。
给一个简短的定义:
For…in 循环处理对象的非符号可枚举属性(这里的关键字是“对象”,因为 JavaScript 中的几乎所有内容都是对象)。当你将自定义对象用作哈希图或字典时(这是非常常见的做法),这非常有用。
但请 注意,迭代是按任意顺序完成的,因此不要依赖循环来选择所需的正确顺序,并且在有必要的时候一定要自己控制好这个部分。
let myMap {
uno: 1,
dos: 2,
tres: 3
}
for(let key in myMap) {
console.log(key, “=”, myMap[key]);
}
/
uno = 1
dos = 2
tres = 3
/

但请注意,因为就像我说的那样,JavaScript 中的几乎所有内容都是一个对象,因此你可以在确实需要 For…of 的时候结束 For…in
例如,如果你要遍历一个字符串(它是一个对象)中的每个字符,那么如果你使用 For…in 将会发生以下情况:
for (let k in “Hello World!”) {
console.log(k)
}
/
0
1
2
3
4
5
6
7
8
9
10
11
/
我们没有遍历字符串的每个字母,而是遍历了每个属性;正如你所看到的,我们实际上是在处理与数组非常相似的结构(对于字符串类型)。这是有道理的,因为”Hello World!”[1] 不仅能正常执行,还可以返回这个位置的实际字符(也就是字母“e”)。相反,如果你想遍历每个字符,则需要使用另一个变体:For…of
for (let char of “Hello World!”) {
console.log(char)
}
/
H
e
l
l
o
W
o
r
l
d
!
/
现在是不是更清楚一些了?用例是一样的,但是有了它,你就可以访问可迭代的值(字符串是可迭代的,数组、映射、集合和 arguments 或 NodeList 这样类似数组的结构也是可迭代的);当然也包括你自己的对象,只要把它们定义为可迭代即可。拿上面的示例来说,没有直接的方法来获取循环的当前索引,当然除非你在循环之外定义它并在每个步骤上都更新它;或者你可以针对数组使用 entries 方法,则可以同时获取索引和值,如下所示:
let myArr = [“hello”, “world”]
for ([idx, value] of myArr.entries()) {
console.log(idx, ‘=’, value)
}
/
0 ‘=’ ‘hello’
1 ‘=’ ‘world’
/
最后作为对比,看看异步代码的情况:一模一样!
const fs @= require(“fs”)
async function read(fname) {
return new Promise((resolve, reject) => {
fs.readFile(fname, (err, content) => {
if (err) return reject(err)
resolve(content.toString())
})
})
}
(async () => {
let files = [‘file2.json’, ‘file2.json’]
for (fname of files) {
let fcontent = await read(fname)
console.log(fcontent)
console.log(“———-“)
}
for (idx in files) {
let fcontent = await read(files[idx])
console.log(fcontent)
console.log(“———-“)
}
})()
两个循环使用 await 构造时的反应方式完全相同,从而使你可以编写更简单、更简洁的代码。

3.优雅的函数式.forEach 循环

这可能是我最喜欢的一个,只是因为我非常喜欢声明式语法或通过声明式风格代替命令式来编写代码。而且,尽管前面两个版本的循环很好用,并且都有很好的用例,但它们也是非常命令式的风格,我们需要编写 我们的数据应该发生什么事情,而不是简单地编写 我们想要数据发生什么事情
不管怎么说,.forEach 方法是 For 循环的另一个版本;但这种方法是数组对象的一部分,并且在执行函数时要接收一个函数和一个额外的可选参数来重新定义函数的上下文。
对于数组中的每个元素,我们的函数都会执行,并且会收到 三个参数(是的,确实是 三个,而不是你习惯使用的一个)。它们分别是:

  1. 当前正在处理的元素。
  2. 元素的索引,这简化了我们尝试使用 For…of 循环完成的任务
  3. 实际正在处理的数组。以防万一你要对它做什么事情。

下面我们来看一个简单的示例:
a = [“hello”, “world”]
a.forEach((elem, idx, arr) => {
console.log(elem, “at: “, idx, “inside: “, arr)
})
/
hello at: 0 inside: [ ‘hello’, ‘world’ ]
world at: 1 inside: [ ‘hello’, ‘world’ ]
/
简单明了,你可以看到我们是怎样在函数内轻松使用所有属性的。下面这个示例针对的是,你希望在 forEach 方法上使用第二个可选参数的情况:
class Person {
constructor(name) {
this.name = name
}
}
function greet(person) {
console.log(this.greeting.replace(“$”, person.name))
}
let english = {
greeting: “Hello there, $”
}
let spanish = {
greeting: “Hola $, ¿cómo estás?”
}
let people = [new Person(“Fernando”), new Person(“Federico”), new Person(“Felipe”)]
people.forEach(greet, english)
people.forEach(greet, spanish)
通过覆盖被调用函数 greet 的上下文,我可以在不影响其代码的情况下更改其行为。最后,为了表明这一方法也可以与异步代码一起使用,下面是示例:
const fs = require(“fs”)
async function read(fname) {
return new Promise((resolve, reject) => {
fs.readFile(fname, (err, content) => {
if (err) return reject(err)
resolve(content.toString())
})
})
}
let files = [‘file1.json’, ‘file2.json’]
files.forEach(async fname => {
let fcontent = await read(fname)
console.log(fcontent)
console.log(“———-“)
})
请注意,由于我将回调声明为异步,因此不再需要 IIFE。

总结

我要分享的关于 JavaScript 中 For 循环的全部信息就是这些了,我希望现在你对它们有了更清晰的了解,并可以基于这些知识和当前的编码需求来选择自己喜欢的循环。