Skip to content

函数式编程不是“用 map 替代 for 循环”

——它是一种以函数为基本构造单元的编程范式,核心是表达“做什么”而非“怎么做”

“如果你只是把 for 换成 map,那不过是给马车装上碳纤维——结构没变,只是更快地走向混乱。”

一、一个残酷的真相:90% 的“函数式代码”其实是伪声明式

我们常看到这样的代码:

js
// 看似“函数式”
users
  .filter(u => u.active)
  .map(u => ({ ...u, premium: true }))
  .forEach(u => db.save(u));

开发者说:“我用了 map/filter/forEach,所以我是函数式编程。”

错。

这段代码的问题是:

  • forEach 强制执行副作用(保存数据库)
  • map 返回新数组,但被 forEach 丢弃
  • 整个链式调用是一个指令式过程:先过滤,再映射,最后执行

它只是把 for 循环的语法换成了链式调用,思维模式仍是“一步步怎么做”

二、真正的函数式:从“如何做”到“是什么”的跃迁

指令式(Imperative):描述步骤

js
const result = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].active) {
    const upgraded = { ...users[i], premium: true };
    result.push(upgraded);
    db.save(upgraded); // 副作用嵌入逻辑
  }
}

关注点:索引、循环、条件、push、保存……

声明式(Declarative):描述结构与关系

js
// 定义“活跃用户升级”这件事是什么
const upgradeUser = user => ({ ...user, premium: true });
const isActive = user => user.active;
const saveToDb = user => () => db.save(user); // 返回一个“效果”

// 组合:活跃用户的升级版本,保存到数据库
const processActiveUsers = flow(
  filter(isActive),
  map(upgradeUser),
  map(saveToDb) // 不执行,只构建效果
);

现在,processActiveUsers(users) 返回的是一组延迟执行的 IO 操作

你还没有“做”任何事,你只是定义了“要做什么”

三、“做什么” vs “怎么做”:两种思维模型的本质差异

维度指令式(怎么做)声明式(做什么)
焦点控制流(if, for, while)数据转换与组合
时间性顺序执行,有“先后”关系描述,无时间依赖
可替换性不能替换中间步骤表达式可替换(引用透明)
组合性复制粘贴逻辑,难以复用函数即值,可组合、可抽象
推理难度必须模拟执行才能理解通过代数化简即可推理

示例:计算折扣价

js
// 指令式:怎么做
function getFinalPrice(price, user) {
  let final = price;
  if (user.isVIP) final *= 0.8;
  if (hasCoupon(user)) final *= 0.9;
  if (final < 10) final += 2; // 运费
  return final;
}

// 声明式:是什么
const applyVIPDiscount = price => price * 0.8;
const applyCoupon = price => price * 0.9;
const addShipping = price => price < 10 ? price + 2 : price;

const applyDiscounts = pipe(
  when(isVIP, applyVIPDiscount),
  when(hasCoupon, applyCoupon),
  addShipping
);

applyDiscounts 不是一个“算法”,而是一个价格变换规则的声明

四、函数作为“构造单元”:为什么这改变了软件的构建方式?

在函数式编程中,函数不是“工具”,而是第一类公民构造块

1. 函数可以组合(Composition)

js
const f = compose(log, validate, parse);
// f(x) 等价于 log(validate(parse(x)))

组合不是“调用顺序”,而是数学意义上的函数复合

2. 函数可以抽象(Abstraction)

js
const retry = (times, action) => async () => {
  for (let i = 0; i < times; i++) {
    try { return await action(); }
    catch (e) { if (i === times - 1) throw e; }
  }
};

// 抽象出“重试”这个概念
const fetchWithRetry = retry(3, () => fetch('/api/data'));

retry 是一个高阶函数,它接受一个“做什么”(action),返回一个新的“做什么”(带重试的行为)。

3. 函数可以延迟(Lazy Evaluation)

js
const numbers = range(1, Infinity);
const evens = filter(n => n % 2 === 0, numbers);
const firstTen = take(10, evens);

// 只有当你消费 `firstTen` 时,计算才真正发生

你不是在“一步步生成偶数”,而是在声明“前十个偶数”这个概念

五、JS 中的实践:如何写出真正的“做什么”代码?

1. 避免立即执行副作用

错误:

js
users.map(sendEmail); // 立即发送邮件

正确:

js
const emailActions = users.map(user => () => sendEmail(user));
// 现在你可以:
// - 过滤某些用户
// - 批量执行
// - 记录日志
// - 模拟测试

2. 使用 pipe / compose 表达数据流

js
const processOrder = pipe(
  validateOrder,
  calculateTax,
  applyDiscounts,
  persistToDB,
  triggerShipping
);

这不是“调用五个函数”,而是定义一个订单处理流程

3. 用代数数据类型(ADT)建模业务概念

js
const OrderStatus = {
  Pending: () => ({ type: 'Pending' }),
  Confirmed: (at) => ({ type: 'Confirmed', at }),
  Shipped: (tracking) => ({ type: 'Shipped', tracking })
};

// 状态不再是字符串,而是一个可模式匹配的结构

六、为什么这带来可维护性?

因为**“做什么”是稳定的,“怎么做”是易变的**。

  • 业务需求:“VIP 用户打 8 折” → 稳定
  • 实现细节:“先判断 VIP,再乘 0.8” → 易变

当你用函数式表达“做什么”,你可以:

  1. 独立测试每个小函数
  2. 自由重组业务流程
  3. 在不改变语义的情况下优化性能
  4. 用数学等价替换重构代码

系统变得像乐高,而不是胶水粘起来的纸板箱。

结语:函数式编程是一场“认知革命”

它强迫你从“控制计算机”转向“描述世界”。

mapfilterreduce 只是语法糖。
真正的函数式编程,是用函数作为语言,去建模问题域的本质结构

当你能写出:

js
const userJourney = compose(
  onboard,
  engage,
  retain,
  monetize
);