Code Review 惊魂:同事的“优雅”重构,差点让管理员全部掉线

差点就酿成了"生产事故"

昨天下午,组里的实习生小李提交了一个 PR,说是把权限管理模块的代码"优化"了一下。

我扫了一眼 Diff,绿油油的一片,看起来确实清爽了不少。代码风格从原本的命令式变成了函数式,逻辑似乎也没啥大问题。手指悬在 "Approve" 按钮上,正准备点下去,突然看到一行对数组的处理,心里咯噔了一下。

赶紧把代码拉到本地跑了一遍------果然,原本正常的权限列表,重构后直接变成了空数组!

如果这行代码真的上线了,明天所有的管理员都会因为没有权限而被拦在后台门外,那我们组这个季度的绩效怕是要集体泡汤。

小李改动的地方很简单,他觉得原来的 push 写法太繁琐,想用 concat 让他变得"优雅"一点。

于是代码从:

javascript 复制代码
// 老代码         
defaultRoles.forEach(role => {
  userPermissions.push(role);
});

变成了:

javascript 复制代码
// 新代码:
userPermissions.concat(defaultRoles);

我把他叫到工位上,指着这行代码问他:"你觉得这两段代码等价吗?"

他一脸茫然:"不都是把数组拼起来吗?concat 不是更符合函数式编程习惯吗?"

看来,是时候聊聊 JS 数组方法里那些容易让人"阴沟里翻船"的返回值陷阱了。

还原一下"案发现场"

原始逻辑(正常工作)

我们的业务逻辑大概是这样的:从后端拉取用户的角色,然后把对应的权限塞到一个数组里。

javascript 复制代码
// 伪代码:将嵌套的权限数组扁平化
// 以前是用 reduce + push 写的,虽然丑点,但能用
export const flatPermissions = allPermissions.reduce((acc, role) => {
    // ... 省略中间处理逻辑
    // 把处理好的权限 push 进累加器
    acc.push(...processedPermissions); 
    return acc;
}, []);

这段代码跑了一年多,稳如老狗。

重构后的代码(引入 Bug)

小李为了追求代码整洁,在重构时把 push 换成了 concat

javascript 复制代码
export const flatPermissions = allPermissions.reduce((acc, role) => {
    // ... 省略中间处理逻辑
    
    // 他以为这行代码会把新权限加到 acc 里
    acc.concat(processedPermissions); 
    
    return acc;
}, []);

看起来逻辑没变,对吧?

但在 JS 引擎眼里,这行代码的意思是:

  1. 拿出 acc 数组。
  2. 创建一个新数组 ,内容是 acc + processedPermissions
  3. 把这个新数组扔掉(因为没有接收返回值)。
  4. 返回原本的、没有任何变化的 acc

结果就是:flatPermissions 永远是个空数组。

到底哪里出了问题?

我给小李画了张图,解释这俩方法的本质区别。

concat vs push:本质的区别

这俩方法的区别,不仅仅是写法不同,而是设计哲学完全不同。

1. concat:我只产生新东西,不碰旧东西

concat 是**非变异(Non-mutating)**方法。它不会修改调用它的数组,而是返回一个新的。

javascript 复制代码
const arr1 = [1, 2];
const arr2 = [3, 4];

// 错误用法:以为 arr1 变了
arr1.concat(arr2); 
console.log(arr1); // [1, 2] -> 根本没动!

// 正确用法:必须接收返回值
const result = arr1.concat(arr2);
console.log(result); // [1, 2, 3, 4]

2. push:我就改旧东西

push变异(Mutating)方法。它直接修改原数组,返回的是新数组的长度(这个返回值也经常坑人)。

javascript 复制代码
const arr1 = [1, 2];
const arr2 = [3, 4];

// push 修改了 arr1
const length = arr1.push(...arr2); 

console.log(arr1); // [1, 2, 3, 4] -> 变了!
console.log(length); // 4 -> 返回的是长度

内存和引用对比

为了加深印象,咱们看个图:

graph TD subgraph Push操作 A[原数组 arr1] -->|push| A A -.->|变大了| A end subgraph Concat操作 B[原数组 arr1] -->|concat| C[新数组 result] B -.->|保持原样| B end
  • Push: 在原数组内存地址上扩容。
  • Concat: 申请新内存,复制旧数据,复制新数据,返回新地址。

怎么修?这有三招

方案一:继续用 push,配合展开运算符

这是性能最好的改法,虽然看起来稍微没那么"函数式"。

javascript 复制代码
export const flatPermissions = allPermissions.reduce((acc, role) => {
    // 使用展开运算符 ... 把新数组打散塞进去
    acc.push(...role.permissions);
    return acc;
}, []);

优点 :性能好,内存抖动少。 缺点 :修改了 acc(但在 reduce 内部通常是可以接受的)。

方案二:正确使用 concat

如果你坚持要用 concat,记得把返回值接住。

javascript 复制代码
export const flatPermissions = allPermissions.reduce((acc, role) => {
    // 返回新的数组给下一次迭代
    return acc.concat(role.permissions);
}, []);

优点 :纯函数式,不修改原数组。 缺点:在循环中频繁创建新数组,可能带来额外的内存开销。

方案三:用 flatMap(推荐)

既然是"映射"+"扁平化",JS 早就给我们准备好了专用 API。

javascript 复制代码
export const flatPermissions = allPermissions.flatMap(role => role.permissions);

优点 :代码最简洁,语义最清晰,专门干这事的。 缺点:老旧浏览器(如 IE)不支持,需要 Polyfill。

避坑指南:还有哪些方法是"只读"的?

concat 只是冰山一角。JS 的数组方法里,修改原数组返回新数组的方法经常让人晕头转向。

我整理了一份清单,建议背下来,或者贴在电脑屏幕旁边:

⚠️ 会修改原数组(Mutating)

  • push() / pop()
  • unshift() / shift()
  • splice() (这个最容易混淆!)
  • sort()
  • reverse()
  • fill()

✅ 只返回新数组(Non-Mutating)

  • concat()
  • slice() (注意是 slice 不是 splice)
  • map() / filter()
  • reduce()
  • toSorted() / toReversed() / toSpliced() (ES2023 新出的"安全版"方法)

写在最后

那天下午,我让小李把代码改了回去,并顺便给他科普了一波数组方法的副作用。

Code Review 的意义其实就在这里:不仅是找 Bug,更是团队技术栈的校准和经验的传承。

对于每一个开发者来说,写代码不能光图"看着顺眼"。在把 push 换成 concat 之前,先问自己两个问题:

  1. 我接收返回值了吗?
  2. 这代码是在循环里吗?

希望这次"险些发生"的生产事故,能给你的 Code Review 清单里增加一项检查点。


相关阅读

相关推荐
虚伪的空想家3 小时前
arm架构服务器使用kvm创建虚机报错,romfile “efi-virtio.rom“ is empty
linux·运维·服务器·javascript·arm开发·云原生·kvm
0***K8923 小时前
Vue数据挖掘开发
前端·javascript·vue.js
Irene19914 小时前
ES6 export 语句 语法规范
javascript·es6·export
H***99764 小时前
Vue深度学习实战
前端·javascript·vue.js
猴猴不是猴4 小时前
js实现卷轴,中间可滑动方块,左右两侧对比
javascript·css·css3
toooooop84 小时前
Vuex 中 state、mutations 和 actions 的原理和写法
前端·javascript·uni-app
y***86694 小时前
前端CSS-in-JS方案
前端·javascript·css
华仔啊4 小时前
你真的懂递归吗?没那么复杂,但也没那么简单
前端·javascript
专业抄代码选手5 小时前
【Leetcode】1930. 长度为 3 的不同回文子序列
javascript·算法·面试