遍历集合,会产生副作用。——如 mori.each 文档所说
首先声明,本文和性能无关。执行 for
循环总是比执行 Array.forEach
快。如果性能测试显示迭代的开销足够显著并且性能优先,那么你绝对应该使用 for
循环而不是 forEach
(总是使用 for
循环是典型的过早优化。forEach
仍然可以在 1 微秒内遍历长度为 50 的数组)。本文和编码风格有关,是我对 forEach
和其它 Array.prototype
方法的思考,与性能无关。
forEach 为副作用而生
当人们想要把代码重构成一个更加实用的风格时,往往首选 [].forEach
或 _.each
。forEach
直接模拟最基本的 for
循环——遍历数组并且执行一些操作——所以它是一个很机械的转换。但是就像 for
循环一样,forEach
在程序的某个地方必定会产生副作用。它必须修改父作用域中的对象,或者调用一个外部方法,或者使用迭代函数以外的其它变量。使用 forEach
也意味着你的迭代函数和它所在的作用域产生了耦合。
在编程中,我们通常认为副作用是不好的。他们使程序更难理解,可能导致 bug 的产生,而且难以重构。当然,forEach
在大项目中引起的副作用是微不足道的,但是这些副作用是不必要的。
当然也有一些副作用是无法避免的。
1 2 3 |
arr.forEach(function (item) { console.log(item); }); |
这种情况完全可以接受。
forEach
隐藏了迭代的意图
阅读 forEach
代码段的时候,你并不能马上知道它的作用,只知道它会在某个地方产生副作用,然后必须阅读这段代码或者注释才明白。这是一个非语义的方法。
除了 forEach
,还有更好的迭代方法。比如 map
——在使用迭代函数以后会返回一个新数组;比如 filter
——返回由符合条件的元素组成的新数组;比如 some
(或者 _.any
)——如果数组中至少有一个元素满足要求时返回 true
;比如 every
(或者_.all
)——如果数组中所有元素满足要求时返回 true
;比如 reduce
——遍历数组并且使用数组中的所有元素进行某种操作迭代生成一个新的变量,数组中的很多方法都可以用 reduce
来实现。ES5 的数组方法非常强大,希望你对此并不陌生。Lodash/Underscore 库增强了 ES5 的方法,增加了很多有用且语义化的迭代函数(此外还提供了可用于对象的数组原型方法的更优实现)。
重构
下面是一些实际项目中使用 each
的例子,看看如何更好地重构它们。
例 1
1 2 3 4 5 |
var obj = {}; arr.forEach(function (item) { obj[item.key] = item; }); |
这是一个很常见的操作——将数组转换为对象。由于迭代函数依赖 obj
,所以 forEach
跟它所在的作用域耦合在一起。迭代函数不能在它的闭包作用域之外执行。我们换个方式来重写它:
1 2 3 4 |
var obj = arr.reduce(function (newObj, item) { newObj[item.key] = item; return newObj; }, {}); |
现在归并函数只依赖于它的形参,没有别的。reduce
无副作用——遍历集合,并且只产出一个东西。它是 ES5 方法中最不语义的方法,但它很灵活,可以用来实现所有其余的函数。
Lodash 还有更语义化的写法:
1 |
var obj = _.zipObject(_.pluck(arr, 'key'), arr); |
这里需要遍历2次,但是看起来更直观。
译者注:实际上有更好的方法
1 var obj = _.indexBy(arr, 'key');
例 2
1 2 3 4 5 6 |
var replacement = 'foo'; var replacedUrls = urls; urls.forEach(function replaceToken(url, index) { replacedUrls[index] = url.replace('{token}', replacement); }); |
用 map
重构:
1 2 3 4 5 6 |
var replacement = 'foo'; var replacedUrls; replacedUrls = urls.map(function (url) { return url.replace('{token}', replacement); }); |
map
函数仍然依赖于 replacement
的作用域,但是迭代的意图更加清晰。前一种方法改变了 urls
数组,而 map
函数则分配了一个新的数组。需要注意的是,它对 urls
的修改不易被察觉,其它地方可能仍然期望 urls
中会含有 {token}
。采用分配新数组的方法可以防止这个小细节引发的问题,代价就是需要多一点内存开销。
例 3
遍历集合,会产生副作用。——如 mori.each 文档所说
首先声明,本文和性能无关。执行 for
循环总是比执行 Array.forEach
快。如果性能测试显示迭代的开销足够显著并且性能优先,那么你绝对应该使用 for
循环而不是 forEach
(总是使用 for
循环是典型的过早优化。forEach
仍然可以在 1 微秒内遍历长度为 50 的数组)。本文和编码风格有关,是我对 forEach
和其它 Array.prototype
方法的思考,与性能无关。
forEach 为副作用而生
当人们想要把代码重构成一个更加实用的风格时,往往首选 [].forEach
或 _.each
。forEach
直接模拟最基本的 for
循环——遍历数组并且执行一些操作——所以它是一个很机械的转换。但是就像 for
循环一样,forEach
在程序的某个地方必定会产生副作用。它必须修改父作用域中的对象,或者调用一个外部方法,或者使用迭代函数以外的其它变量。使用 forEach
也意味着你的迭代函数和它所在的作用域产生了耦合。
在编程中,我们通常认为副作用是不好的。他们使程序更难理解,可能导致 bug 的产生,而且难以重构。当然,forEach
在大项目中引起的副作用是微不足道的,但是这些副作用是不必要的。
当然也有一些副作用是无法避免的。
1 2 3 |
arr.forEach(function (item) { console.log(item); }); |
这种情况完全可以接受。
forEach
隐藏了迭代的意图
阅读 forEach
代码段的时候,你并不能马上知道它的作用,只知道它会在某个地方产生副作用,然后必须阅读这段代码或者注释才明白。这是一个非语义的方法。
除了 forEach
,还有更好的迭代方法。比如 map
——在使用迭代函数以后会返回一个新数组;比如 filter
——返回由符合条件的元素组成的新数组;比如 some
(或者 _.any
)——如果数组中至少有一个元素满足要求时返回 true
;比如 every
(或者_.all
)——如果数组中所有元素满足要求时返回 true
;比如 reduce
——遍历数组并且使用数组中的所有元素进行某种操作迭代生成一个新的变量,数组中的很多方法都可以用 reduce
来实现。ES5 的数组方法非常强大,希望你对此并不陌生。Lodash/Underscore 库增强了 ES5 的方法,增加了很多有用且语义化的迭代函数(此外还提供了可用于对象的数组原型方法的更优实现)。
重构
下面是一些实际项目中使用 each
的例子,看看如何更好地重构它们。
例 1
1 2 3 4 5 |
var obj = {}; arr.forEach(function (item) { obj[item.key] = item; }); |
这是一个很常见的操作——将数组转换为对象。由于迭代函数依赖 obj
,所以 forEach
跟它所在的作用域耦合在一起。迭代函数不能在它的闭包作用域之外执行。我们换个方式来重写它:
1 2 3 4 |
var obj = arr.reduce(function (newObj, item) { newObj[item.key] = item; return newObj; }, {}); |
现在归并函数只依赖于它的形参,没有别的。reduce
无副作用——遍历集合,并且只产出一个东西。它是 ES5 方法中最不语义的方法,但它很灵活,可以用来实现所有其余的函数。
Lodash 还有更语义化的写法:
1 |
var obj = _.zipObject(_.pluck(arr, 'key'), arr); |
这里需要遍历2次,但是看起来更直观。
译者注:实际上有更好的方法
1 var obj = _.indexBy(arr, 'key');
例 2
1 2 3 4 5 6 |
var replacement = 'foo'; var replacedUrls = urls; urls.forEach(function replaceToken(url, index) { replacedUrls[index] = url.replace('{token}', replacement); }); |
用 map
重构:
1 2 3 4 5 6 |
var replacement = 'foo'; var replacedUrls; replacedUrls = urls.map(function (url) { return url.replace('{token}', replacement); }); |
map
函数仍然依赖于 replacement
的作用域,但是迭代的意图更加清晰。前一种方法改变了 urls
数组,而 map
函数则分配了一个新的数组。需要注意的是,它对 urls
的修改不易被察觉,其它地方可能仍然期望 urls
中会含有 {token}
。采用分配新数组的方法可以防止这个小细节引发的问题,代价就是需要多一点内存开销。