为啥 forEach 比常规迭代器更受欢迎?

Posted

技术标签:

【中文标题】为啥 forEach 比常规迭代器更受欢迎?【英文标题】:Why should forEach be preferred over regular iterators?为什么 forEach 比常规迭代器更受欢迎? 【发布时间】:2018-08-31 10:22:32 【问题描述】:

我正在阅读airbnb javascript guide。有一个特别的说法,说:

不要使用迭代器。更喜欢 JavaScript 的高阶函数,而不是 for-in 或 for-of 之类的循环。

他们给出上述说法的原因是:

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

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

    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 有什么副作用吗?

【问题讨论】:

更具可读性?更容易组合? 你会感到惊讶。 For 循环并不容易学习。 Foreach 是。 【参考方案1】:

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 中显着改进的那个(几乎是两倍,基于 this 综合测试):

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

forEachfor..of 相比的主要优点是前者即使在 ES3 中也可以进行 polyfill 并提供值和索引,而后者更具可读性但应该在 ES5 及更低版本中进行转译。

forEach 存在已知的陷阱,使其在某些情况下不适合使用 forfor..of 进行妥善处理:

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

不支持迭代器

不支持生成器yieldasync..await

没有提供正确的方法来使用break 提前终止循环

【讨论】:

我没看懂开头行This reasoning in Airbnb style guide applies to array methods that are used for immutability, which are filter, map, reduce, etc. but not forEach:为什么forEach不能用于不变性? 可以的。但它本质上不是一成不变的。不会自动为您提供新值(例如 reducemap)。您在其他地方定义的循环内改变了一个变量(在您的示例中为sum)。您可以以相同的方式修改现有对象,从而打破“不可变规则”(好吧,您也可以使用 reduce 来做到这一点,但这通常被认为是一个错误)。 forforEach 同样是“坏”的,根据这个推理,forEach 有一个函数这一事实在这方面不会影响任何事情,因为我们不能从这个函数返回结果。跨度> 我认为这个推理是有效的(尽管通常更喜欢for..of 以提高可读性),但不是他们得出的结论。 github.com/airbnb/javascript#iterators-and-generators章节自相矛盾,前后矛盾。在一个地方,它声明应该避免副作用,然后他们在 forEach 中产生副作用并将其标记为“好”。请记住,这只是风格指南。风格指南是固执己见的,并不总是有意义。【参考方案2】:

这没有任何意义。 forEach 永远不应该被首选。是的,使用 mapreducefilter 比使用副作用来操作某些东西的循环要干净得多。

但是当你出于某种原因需要使用副作用时,for … infor … of 是惯用的循环结构。它们更容易阅读和just as fast,并强调你有一个带有副作用的循环体,而forEach 看起来可以通过它的回调来运行,但实际上并没有。与forEach 相比的其他优点包括您可以将const 用于迭代元素,并且可以在循环体中使用任意控制结构(returnbreakcontinueyieldawait) .

【讨论】:

n.b. Eric Lippert 在 C# 中对 foreach.ForEach(...) 的基本相同:blogs.msdn.microsoft.com/ericlippert/2009/05/18/… 【参考方案3】:

大部分时间,Airbnb 风格指南都试图保持一致。这并不意味着永远没有理由使用 for 循环或 for-in 循环,比如假设您想尽早跳出循环。

当使用 for 循环改变某物的值时,不变性就发挥了作用。就像将数组简化为整数或映射元素以生成新数组一样。使用内置的mapreducefilter 不会直接直接改变数组的值,因此它是首选。在 for/for-in 循环上使用 forEach 可以强制在迭代器上使用高阶函数的样式一致性,所以我相信这就是推荐它的原因。

【讨论】:

您能解释一下This isn't the same for forEach 的声明吗?与mapreduce 等其他方法相比,为什么forEach 会改变数组的值? forEach 本身不会改变数组的值。我的意思是,与mapfilter 不同,forEach 将始终返回undefined,它没有返回值并且可以用来改变数组(应该更好地澄清这一点)。已更新。【参考方案4】:

forEach 迭代不能被await 延迟。这意味着您不能使用 forEach 来管道,比如说,向服务器发送 Web 请求。

forEach 也不存在于 async 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] 数组的元素每两秒打印一次

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

【讨论】:

【参考方案5】:

以下是我看到的两者之间的优势列表。

“for of”的优点:

    在开发工具中,你可以通过鼠标悬停查看函数中所有变量的值。 (使用forEach,您需要更改您所在的堆栈条目以查看未在循环函数中访问的变量) 适用于所有可迭代对象(不仅仅是数组)。 你可以直接使用breakcontinue,而不需要使用some函数和return true,这样会降低可读性。 (可以是emulated) 您可以在循环中使用await,同时保留异步/等待上下文。 (可以是emulated) 循环内的代码可以直接从整个函数中返回。 (可以是emulated)

“forEach”的优点

    是像mapfilter这样的函数调用,所以更容易在两者之间转换。 在 es2015 之前的上下文中使用它不需要转译(只需要一个简单的 polyfill)。这也意味着您不必处理来自转译循环的奇怪“错误捕获”,这会使开发工具中的调试变得更加困难。 您可以创建具有附加功能的“自定义 forEach”方法。 (here 是增加了对breakcontinuereturn 甚至async/await 的支持的版本) 有点短。 (如果使用constfor-of 多 5 个字符 -- 如果使用 let/var,则多 3 个字符)

【讨论】:

【参考方案6】:

在此处查看Performance of foreach and forloop

我们使用 foreach 只是为了可读性。 除了浏览器兼容性、性能和原生感觉,更喜欢 for 循环。

【讨论】:

您提供的链接表明forEach 要快得多。 “没有区别” - 他说的是 V8 引擎。这就是区别。在 safari 上 for 始终比 foreach @Bergi 有区别。 AIK,for > forEach > for..of > for..in 就性能而言,这在几乎所有引擎中都是统一的。仍然没有奇偶校验,您可以自己查看jsperf.com/for-vs-foreach-vs-for-of。有趣的是,forEach 在最新的 Chrome 版本中得到了显着改进。

以上是关于为啥 forEach 比常规迭代器更受欢迎?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 NodeJs 开发中 Mongodb 比 MySql 更受欢迎? [关闭]

在 C 系列中,为啥在循环中“小于或等于”比“小于”符号更受欢迎? [关闭]

什么是迭代器?

迭代器 生成器

python之迭代器

为啥我不能在 foreach 循环中设置迭代变量的属性?