最近写代码的时候,忽然发现我已经很长时间没用过forEach
了,甚至看别人在代码里写的forEach
也有种别扭的感觉,总感觉不够优雅,于是我在项目里全局查看了一下所有写 forEach
的地方,最终得出一个结论:90%
的 forEach
是可以被更可维护、更技术性的编码方式给替换掉的
组装变量
大部分使用 forEach
的地方都是为了组装变量,比如组装数组、组装对象
以下是个很常见的初始化数组的例子
ts
function fn(rows: { data: Record<string, string>; info: Record<string, string> }[]) {
let tempRoomInfos: Array<any> = []
rows.forEach(item => {
let tempRoomInfo = { ...item.data, ...item.info }
tempRoomInfos.push(tempRoomInfo)
})
// ...
}
这5
行代码我看了之后心里就叹了口气,第一眼就发现 tempRoomInfo
这个变量根本没存在的必要,搁这凑行数呢,然后又发现这个 forEach
只是为了组装 tempRoomInfos
,最后又发现 tempRoomInfos
这个变量在写类型的时候可能是为了省事居然定义成了 anyScript
,函数体里的五行代码其实一行就完事
ts
function fn(rows: { data: Record<string, string>; info: Record<string, string> }[]) {
const tempRoomInfos = rows.map(item => ({ ...item.data, ...item.info }))
// ...
}
优点不仅是代码少,而且更可读,不需要多少思考就能知道 tempRoomInfos
这个变量是个跟 rows
直接相关的数组,两者长度完全相同,tempRoomInfos
的数组项的 key
也跟 rows
直接关联,另外在这里 tempRoomInfos
完全可以定义成 const
,并且会自动推导出类型,根本没必要为了躲懒定义出 Array<any>
这种类型来
在组装数组的时候,可能还需要有些判断语句
ts
function fn(rows: { data: Record<string, string>; info: Record<string, string> }[]) {
let tempRoomInfos: Array<any> = []
rows.forEach(item => {
if (item.data.key === 'video') {
let tempRoomInfo = { ...item.data, ...item.info }
tempRoomInfos.push(tempRoomInfo)
}
})
// ...
}
依旧可以更优雅,但一个 map
就不够了,可以加个filter
ts
function fn(rows: { data: Record<string, string>; info: Record<string, string> }[]) {
const tempRoomInfos = rows.filter(item => item.data.key === 'video').map(item => ({ ...item.data, ...item.info }))
// ...
}
什么?嫌遍历两次有点浪费,问题不大,上 reduce
,只不过这个时候,你要自己写类型了
ts
function fn(rows: { data: Record<string, string>; info: Record<string, string> }[]) {
const tempRoomInfos = rows.reduce<Record<string, string>[]>((t, item) => {
return t.concat(item.data.key === 'video' ? { ...item.data, ...item.info } : [])
}, [])
// ...
}
对象的组装同样可以更优雅
ts
type TConfigMap = {
[key: string]: {
value: number
valueStr: string
}
}
function fn(rows: { key: string; count: number }[]) {
const configMap: TConfigMap = {}
rows.forEach(item => {
configMap[item.key] = {
value: item.count,
valueStr: `${item.count}%`
};
});
}
这几行代码目的就是为了组装 configMap
这个对象,但没必要将 configMap
的定义与其赋值分开,显得太命令式了,可以更直观点
ts
function fn(rows: { key: string; count: number }[]) {
const configMap = rows.reduce<TConfigMap>((t, item) => {
t[item.key] = {
value: item.count,
valueStr: `${item.count}%`
}
return t
}, {})
}
求和
还有相当一部分用到 forEach
的地方则是为了累加求和
ts
function fn(rows: { count: number }[]) {
let sum = 0
rows.forEach(d => {
sum += d.count
})
}
sum
的定义和初始化分离了,也显得命令式,一个 reduce
优雅又直观
ts
function fn(rows: { count: number }[]) {
const sum = rows.reduce((t, c) => t + c.count, 0)
}
不仅是数字的求和,字符串也可以,例如常见的就是将GET
请求的对象入参转成字符串类型的 query
ts
/**
* query对象转字符串
* @param params query对象
* @example
* query2String({ key: '2', name: 'abc' }) => ?key=2&name=abc
*/
function query2String(params: Record<string, string>) {
let queryStr = '?'
Object.entries(params).forEach(item => {
queryStr += `${item[0]}=${item[1]}&`
})
return queryStr.slice(0, -1)
}
然而,同样的,一个 reduce
就能优雅搞定
ts
function query2String(params: Record<string, string>) {
return Object.entries(params).reduce((t, item) => t + `${item[0]}=${item[1]}&`, '?').slice(0, -1)
}
数组子项属性修改
实际上,这才是我认为应当使用 forEach
的场景
ts
series.forEach(d => {
if (d.type === 'bar') {
d.barWidth = 16
}
})
但是原地修改对象在大型团队协作项目中是一个可能导致 bug
的行为,你把对象给原地修改了,其他用到的地方可能就出错了,而且很难定位问题,所以我一般只会在确定我当前原地修改的对象,是这个对象的唯一出口,不存在更高一级的上游或者同级的时候才会这么干,比如,对一个接口的返回结果进行原地修改再对外输出
上面说到的组装变量,其实有些场景下也不得不使用 forEach
,比如这个对象需要多个逻辑才能完成初始化,比如下属代码,使用 reduce
当然也可以一次完成 newList
的定义和初始化过程,但毕竟不如 forEach
直观,代码是写给人看的,在能跑的前提下,人性化比技巧化更重要
ts
let newList = []
list.forEach(element => {
element.children.forEach(item => {
item.data.forEach(d => {
newList.push(d.data)
})
})
})
小结
本文并不是为了批判 forEach
,而是在批判没有正确使用 forEach
的行为,或者说,在批判没有正确使用编程语言 api
的行为,比如我就见过将 reduce
当成 forEach
来使用的,就是纯粹的遍历,没有用到返回值,让人看了很迷惑,for
循环可以在任何场景下代替数组遍历三兄弟forEach
、map
、reduce
,甚至很多时候用法错了也不是不能跑,但作为一个有技术追求的程序员,我认为优雅的编程行为,是可以有效延缓屎山的堆积速度的