桥豆麻袋,你真的用对了forEach吗?

最近写代码的时候,忽然发现我已经很长时间没用过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循环可以在任何场景下代替数组遍历三兄弟forEachmapreduce,甚至很多时候用法错了也不是不能跑,但作为一个有技术追求的程序员,我认为优雅的编程行为,是可以有效延缓屎山的堆积速度的

相关推荐
下雪天的夏风9 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom20 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan23 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^32 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang37 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫5 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy5 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端