为什么建议把 map 换成 for loop?—— 聊聊 Side Effects 与语义化编程

0 阅读4分钟

在进行代码审查(Code Review)时,你是否收到过这样的评论:

“这里的 side effect 会不会考虑一下用 for loop?”

当时的我心里想:“反正代码都能跑,mapfor 有什么区别吗?map 写起来不是更短更帅吗?”

这篇文章就是为了解答这个疑惑。这不仅仅是代码风格的问题,更关乎代码的可读性(语义)性能(内存)以及函数式编程的原则


一、 什么是 Side Effect(副作用)?

首先,我们要听懂“行话”。在编程中,Side Effect 指的是一个函数在执行过程中,除了返回结果之外,还改变了外部的世界

  • 纯函数 (Pure Function) :输入 2,输出 4。不修改任何外部变量,不读写数据库,不打印日志。像数学公式一样纯粹。

  • 有副作用 (Side Effect)

    • 修改了外部定义的变量(如 total += 1)。
    • 写入数据库(DB Insert/Update)。
    • 发送网络请求(API Call)。
    • 打印日志(console.log)。

Reviewer 的潜台词: “我看你在遍历数组时,并不是为了把 A 变成 B,而是为了拿 A 去做一些操作(比如存库)。这种情况,请不要用 map。”


二、 map vs for:看似一样,实则背道而驰

虽然它们都能遍历数组,但它们在**设计初衷(语义)**上是完全不同的。

1. map 的契约:我只负责“转换”

map 是函数式编程(Functional Programming)的明星工具。它的名字来源于数学中的“映射”(Mapping)。

  • 语义:给我一个数组 [A, B, C],我给你返回一个新数组 [A', B', C']
  • 潜规则一定要有返回值,且不要修改原始数据

2. for / forEach 的契约:我只负责“执行”

for 循环(包括 for...of)是指令式编程(Imperative Programming)的工具。

  • 语义:对着数组里的每一个元素,去执行一段逻辑。
  • 潜规则:我不关心返回值,我只关心过程(比如存库、发邮件)。

三、 为什么用 map 处理副作用被视为 Anti-pattern(反模式)?

如果你写了这样的代码:

// ❌ 被吐槽的写法
const userIds = [1, 2, 3];

userIds.map(id => {
  db.updateStatus(id, 'active'); // <--- 这就是 Side Effect
  // 没有 return,或者 return 了一个没用的东西
});

Reviewer 看到这段代码会有三个层面的担忧:

1. 误导阅读者(语义错误)

代码是写给人看的。 当你使用 map 时,阅读代码的人会预期:“哦,这里在生成一个新的数据列表。” 结果看了一圈发现,你并没有使用 map 的返回值,只是在里面干活。这就像是你雇了一个顶级大厨(map),却让他去送快递(side effect) ——虽然他也能送,但让人觉得很违和。

2. 内存浪费(性能隐患)

这是实打实的技术问题。 map 函数一定会在内存中分配一个新的数组。

  • 如果你用 map 遍历 10,000 个用户去发邮件,通过 map 你会隐式地创建了一个包含 10,000 个 undefined(因为你没 return)的数组。
  • 这个数组创建完立刻就被扔掉等待垃圾回收(GC)。
  • 结论:你在浪费内存,给 GC 增加压力。

3. Async/Await 的陷阱

在后端开发中,这尤为致命。

// ❌ 危险的 map 写法
ids.map(async (id) => {
  await db.delete(id);
});

上面这段代码不会等待数据库删除完成!map 会瞬间执行完,返回一堆 Promise,而你的主程序会继续往下跑。

如果要等待,必须配合 Promise.all

// ✅ 如果要并发,且一定要用 map
await Promise.all(ids.map(async (id) => await db.delete(id)));

但如果你想要按顺序一个一个删(避免把数据库打挂),map 做不到,必须用 for...of


四、 最佳实践:如何修改?

针对 Reviewer 的建议,可以根据场景选择以下两种改法:

场景 A:纯同步操作 / 不需要等待结果

使用 forEachfor...of

// ✅ 语义清晰:我在对每个 ID 执行操作
userIds.forEach(id => {
  console.log(`Processing user ${id}`);
});

场景 B:异步操作(数据库/API)—— 最推荐

现代 JavaScript/TypeScript 开发中,for...of 是处理异步副作用的神器

// ✅ 语义清晰,且支持 await 顺序执行
for (const id of userIds) {
  // 只有上一个 update 完成了,才会执行下一个
  await db.updateStatus(id, 'active'); 
}


五、 总结

下次当你手指放在键盘上准备敲下 .map 时,停下来问自己一个问题:

“我是想要根据旧数组【生成】一个新数组,还是想要用这些数据去【做】一些事情?”

  • 如果是生成数据(转换):请用 map
  • 如果是事情(副作用):请用 for...offorEach

这不仅仅是代码能不能跑的区别,更是从“写代码的人”进阶到“工程师”的必经之路:精准地使用工具,表达清晰的意图。