避免使用forEach

474 查看

遍历集合,会产生副作用。——如 mori.each 文档所说

首先声明,本文和性能无关。执行 for 循环总是比执行 Array.forEach 快。如果性能测试显示迭代的开销足够显著并且性能优先,那么你绝对应该使用 for 循环而不是 forEach(总是使用 for 循环是典型的过早优化。forEach 仍然可以在 1 微秒内遍历长度为 50 的数组)。本文和编码风格有关,是我对 forEach 和其它 Array.prototype 方法的思考,与性能无关。

forEach 为副作用而生

当人们想要把代码重构成一个更加实用的风格时,往往首选 [].forEach 或 _.eachforEach 直接模拟最基本的 for 循环——遍历数组并且执行一些操作——所以它是一个很机械的转换。但是就像 for 循环一样,forEach 在程序的某个地方必定会产生副作用。它必须修改父作用域中的对象,或者调用一个外部方法,或者使用迭代函数以外的其它变量。使用 forEach 也意味着你的迭代函数和它所在的作用域产生了耦合。

在编程中,我们通常认为副作用是不好的。他们使程序更难理解,可能导致 bug 的产生,而且难以重构。当然,forEach 在大项目中引起的副作用是微不足道的,但是这些副作用是不必要的。

当然也有一些副作用是无法避免的。

这种情况完全可以接受。

forEach 隐藏了迭代的意图

阅读 forEach 代码段的时候,你并不能马上知道它的作用,只知道它会在某个地方产生副作用,然后必须阅读这段代码或者注释才明白。这是一个非语义的方法。

除了 forEach,还有更好的迭代方法。比如 map——在使用迭代函数以后会返回一个新数组;比如 filter——返回由符合条件的元素组成的新数组;比如 some(或者 _.any)——如果数组中至少有一个元素满足要求时返回 true;比如 every(或者_.all)——如果数组中所有元素满足要求时返回 true;比如 reduce——遍历数组并且使用数组中的所有元素进行某种操作迭代生成一个新的变量,数组中的很多方法都可以用 reduce 来实现。ES5 的数组方法非常强大,希望你对此并不陌生。Lodash/Underscore 库增强了 ES5 的方法,增加了很多有用且语义化的迭代函数(此外还提供了可用于对象的数组原型方法的更优实现)。

重构

下面是一些实际项目中使用 each 的例子,看看如何更好地重构它们。

例 1

这是一个很常见的操作——将数组转换为对象。由于迭代函数依赖 obj,所以 forEach 跟它所在的作用域耦合在一起。迭代函数不能在它的闭包作用域之外执行。我们换个方式来重写它:

现在归并函数只依赖于它的形参,没有别的。reduce 无副作用——遍历集合,并且只产出一个东西。它是 ES5 方法中最不语义的方法,但它很灵活,可以用来实现所有其余的函数。

Lodash 还有更语义化的写法:

这里需要遍历2次,但是看起来更直观。

译者注:实际上有更好的方法

 

例 2

用 map 重构:

map 函数仍然依赖于 replacement 的作用域,但是迭代的意图更加清晰。前一种方法改变了 urls 数组,而 map 函数则分配了一个新的数组。需要注意的是,它对 urls 的修改不易被察觉,其它地方可能仍然期望 urls 中会含有 {token}。采用分配新数组的方法可以防止这个小细节引发的问题,代价就是需要多一点内存开销。

例 3