桥豆麻袋,你真的用对了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,甚至很多时候用法错了也不是不能跑,但作为一个有技术追求的程序员,我认为优雅的编程行为,是可以有效延缓屎山的堆积速度的

相关推荐
EasyNTS8 分钟前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜2 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点2 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow2 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o2 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā3 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年4 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder4 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727574 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架