为什么应该优先使用 forEach 而不是常规的迭代器?

29

我正在阅读airbnb JavaScript指南,其中有一个特别的声明:

不要使用迭代器。 与for-in或for-of等循环相比,更喜欢JavaScript的高阶函数。

他们对上述声明的原因是:

这强制执行了我们的不可变规则。 处理返回值的纯函数比处理副作用更容易理解。

我无法区分给出的两种编码实践:

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

    // bad
    let sum = 0;
    for (let num of numbers) {
     sum += num;
    }
    sum === 15;

    // good
    let sum = 0;
    numbers.forEach((num) => {
     sum += num;
    });

    sum === 15;

为什么应该优先选择forEach而不是普通的for循环?它真正会有什么区别吗?使用普通的iterators是否会有任何副作用?


更易读?更易组合? - evolutionxbox
2
你会感到惊讶的。for循环并不容易学习,而foreach循环则相对简单。 - evolutionxbox
6个回答

24

在 Airbnb 的代码规范中,这种推理适用于用于不可变性的数组方法,如 filtermapreduce 等,但不适用于 forEach

这强制执行了我们的不可变规则。处理返回值的纯函数比处理副作用更容易理解。

因此,比较更像是:

// bad
let sum = 0;
for (let num of numbers) {
 sum += num;
}
sum === 15;

// bad
let sum = 0;
numbers.forEach((num) => {
 sum += num;
});

sum === 15;

// good
const sum = numbers.reduce((num, sum) => sum += num, 0);

sum === 15;

一般情况下,for > forEach > for..of > for..in 的性能表现是如此。这个关系在几乎所有引擎中都是一致的,但对于不同的数组长度可能有所不同。

forEach 是最近在 Chrome/V8 中显著改进的一个方法(基于合成测试,性能提高了将近两倍):

由于它们都很快,因此仅基于性能原因选择不太合适的循环方法可能被认为是初步优化,除非有其他证明。

forEach 相对于 for..of 的主要优点在于前者可以在 ES3 中进行 polyfill,并提供值和索引,而后者更易读但应在 ES5 及以下版本中转译。

forEach 有已知的缺陷,在某些可以用 forfor..of 正确处理的情况下可能不适用:

  • 回调函数创建新的上下文(可以使用箭头函数来解决)

  • 不支持迭代器

  • 不支持生成器的 yieldasync..await

  • 没有提供一种正确的方法来提前终止循环,例如使用 break


3
这句话的意思是,Airbnb风格指南中的这种推理适用于用于不可变性的数组方法,例如filter、map、reduce等,但不包括forEach。为什么forEach不能用于不可变性呢? - Jatt
2
可以。但它本质上并不是不可变的。新值不会自动提供给您(就像使用reducemap一样)。您在循环内部改变了在其他地方定义的变量(例如您示例中的sum)。您可以以同样的方式修改现有对象,从而打破“不可变规则”(虽然您也可以使用reduce这样做,但通常被认为是错误的)。根据这种推理,“for”和“forEach”同样“糟糕”,forEach具有一个函数并不影响这方面的任何内容,因为我们无法从此函数返回结果。 - Estus Flask
1
我认为这种推理是有效的(虽然出于可读性的考虑我通常更喜欢使用 for..of ),但并不认同他们得出的结论。章节 https://github.com/airbnb/javascript#iterators-and-generators 中存在矛盾和不一致之处。其中一个地方指出应避免副作用,而后面又在 forEach 中使用了副作用并将其标注为“好的”。请记住,这只是风格指南。风格指南具有个人观点,并不总是合理的。 - Estus Flask
1
除非有根本性的原因导致某些结构更难被优化,否则这种基准比较是非常不可靠的。例如,在最新的v8版本10.4.123.20上,我发现for...offorforEach基本上具有相同的性能。 - Meow

16
这没有任何意义。 应该永远不要首选forEach。是的,使用mapreduce以及filter比使用副作用来操作某些内容的循环更加清晰。
但是当您需要出于某种原因使用副作用时,for...infor...of是惯用的循环结构。它们更易于阅读,速度也一样快,并且强调您具有循环体的副作用与forEach形式上使用回调但实际上不是函数式的区别。相对于forEach的其他优点包括可以将const用于迭代元素,并且可以在循环体中使用任意控制结构(returnbreakcontinueyieldawait)。

6
Eric Lippert在他的博客中也谈到了C#中foreach.ForEach(...)之间的区别:https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/。看起来,Airbnb的人似乎陷入了这样的误区,认为外表像函数式编程就更好。 - JLRishe

3
以下是我看到的“for of”和“forEach”的优势列表。
“for of”的优点:
  1. 在开发工具中,只需悬停即可查看函数中所有变量的值。(使用“forEach”,您需要更改哪个堆栈条目以查看未在循环函数内访问的变量)
  2. 适用于所有可迭代的对象(不仅限于数组)。
  3. 您可以直接使用breakcontinue,而无需使用some函数并执行return true,这样可以减少可读性。(可以模拟)
  4. 您可以在循环中使用await,并保留异步/等待上下文。(可以模拟)
  5. 循环内的代码可以直接返回整个函数。(可以模拟)
“forEach”的优点:
  1. 它像 mapfilter 一样是一个函数调用,因此在两者之间转换更容易。
  2. 它不需要转译即可在 pre-es2015 上下文中使用(只需要一个简单的 polyfill)。这也意味着您不必处理来自转译循环的奇怪的“错误捕获”,这使得在开发工具中进行调试更加困难。(取决于转译工具/选项)
  3. 您可以创建一个带有附加功能的“自定义 forEach”方法。(这里 是一个添加对 breakcontinuereturn 甚至是 async/await 支持的版本)
  4. 它稍微短一些。(如果使用 const,则 for-of 要多 5 个字符 -- 如果使用 let/var 则要多 3 个字符)

3

forEach 迭代不能通过 await 延迟。这意味着你不能使用 forEach 来进行例如向服务器发送 Web 请求的管道操作。

另外,forEachasync Iterables 上不存在。

考虑以下情况:

1

let delayed = (value)=>new Promise(resolve => setTimeout(() => resolve(value), 2000));


(async ()=>{
    for(el of ['d','e','f'])console.log(await delayed(el))
    
})();


(async()=>{
    
    ['a','b','c'].forEach(async (el)=>console.log(await delayed(el)))
   
})();

结果:

d
a
b
c
e
f

每两秒打印一次 [d,e,f] 数组的元素。

等待两秒后一起打印 [a,b,c] 数组的元素。

2

let delayed = (value)=>new Promise(resolve => setTimeout(() => resolve(value), 2000));

async function* toDelayedIterable(array) {
    for(a of array)yield (await delayed(a))    
}


for await(el of toDelayedIterable(['d','e','f']))console.log(el)

toDelayedIterable(['a', 'b', 'c']).forEach(async(el)=>console.log(await el));

结果:

d
e
f
Uncaught TypeError: toDelayedIterable(...).forEach is not a function

[d,e,f]数组的元素每两秒钟打印一次。

尝试访问[a,b,c]数组的asyncIterator上的forEach时发生错误。


2
大多数情况下,Airbnb样式指南都会尽力保持一致性。这并不意味着永远没有使用for循环或for-in循环的理由,比如你想要提前退出循环。
当使用for循环来更改某个值时,不可变性就发挥作用了。例如将数组缩减为整数或映射元素以生成新数组。使用内置的map、reduce或filter不会直接改变数组的值,因此更受欢迎。使用forEach而不是for/for-in循环强制执行使用高阶函数而不是迭代器的样式一致性,我认为这就是为什么建议使用它的原因。

你能解释一下语句“This isn't the same for forEach”吗?为什么与其他方法(如map、reduce等)相比,“forEach”会改变数组的值? - Jatt
1
forEach 并不会本质上改变数组的值。我的意思是,与 mapfilter 不同,forEach 总是返回 undefined,它没有返回值,但可以用于改变数组(我应该更好地澄清这一点)。已更新。 - Sharmila Jesupaul

-2

在这里查看foreach和forloop的性能

除了可读性外,我们仅使用foreach。除此之外,为了浏览器兼容性、性能和本地化感觉,for循环更受青睐。


你提供的链接表明,“forEach”更快。 - Jatt
“没有区别” - 他在谈论V8引擎。这就是区别所在。在Safari中,“for”比“foreach”始终更快。 - radarbob
@Bergi 有所不同。AIK,对于> forEach > for..of > for..in在性能方面,这在几乎所有引擎中都是一致的。仍然没有平等,您可以自行检查https://jsperf.com/for-vs-foreach-vs-for-of。有趣的是,forEach是最新Chrome版本中显着改进的那个。 - Estus Flask

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接