Skip to content

2. 纯函数与副作用:构建可靠代码的基石

在上一章,我们初步领略了函数式编程的声明式之美。现在,我们要深入探讨其背后最核心、最强大的基石——纯函数(Pure Functions)副作用(Side Effects)

理解了这两者之间的区别,你就掌握了编写可靠、可预测、易于测试代码的关键。

什么是纯函数?

想象一个数学函数,比如 f(x) = x * 2。无论你何时用 x = 2 来调用它,结果永远是 4。它不会偷偷修改 x 的值,也不会在你的计算器屏幕上留下奇怪的印记。它只是安静地接收输入,然后返回输出。

这就是纯函数。它必须满足两个严格的条件:

  1. 引用透明(Referential Transparency):对于相同的输入,永远返回相同的输出。你可以把函数调用 f(2) 直接替换成结果 4,而不会对程序的任何部分产生影响。
  2. 无副作用(No Side Effects):函数在执行过程中,不会对外部世界产生任何可观察的影响。

让我们看一个 JavaScript 中的纯函数例子:

javascript
// 一个纯函数,计算一个数的平方
const square = (x) => x * x;

// 另一个纯函数,拼接两个字符串
const greet = (name) => `Hello, ${name}!`;

无论你调用 square(3) 多少次,结果永远是 9。它不依赖任何外部状态,也不修改任何东西。它就像一个封闭、独立的计算单元。

副作用:代码中的“不可控因素”

副作用,顾名思义,就是函数在完成其主要工作(返回值)之外,还做了其他事情。这些“其他事情”会与函数外部的环境发生交互,从而引入了不确定性。

常见的副作用包括:

  • 修改全局变量或传入的参数(对象、数组)。
  • 发起网络请求(AJAX/Fetch)。
  • 读写文件或数据库。
  • 操作 DOM 元素。
  • 调用 console.log()alert()
  • 使用 Math.random()new Date()

让我们来看一个充满副作用的“不纯”的函数:

javascript
let user = { name: 'Alice', age: 30 };

// 一个不纯的函数,因为它修改了外部的 user 对象
function celebrateBirthday(person) {
  person.age = person.age + 1; // 副作用!修改了传入的对象
  console.log(`Happy birthday, ${person.name}!`); // 副作用!输出了日志
  // ... 可能还会把 person 保存到数据库
  return person;
}

celebrateBirthday(user);

console.log(user); // { name: 'Alice', age: 31 } -> user 对象被意外修改了!

这个 celebrateBirthday 函数就是一个“危险分子”。它不仅修改了传入的 user 对象,还向控制台打印了信息。如果你在代码的另一个地方也用到了 user 对象,你可能会惊讶地发现它的 age 已经不是你期望的值了。

这种代码难以推理和测试。为了测试它,你不仅要检查它的返回值,还要验证 console.log 是否被调用,以及 user 对象是否被正确地修改。这非常脆弱。

如何驯服副作用?

函数式编程并非要完全消灭副作用。毕竟,一个完全没有副作用的程序什么也做不了——它无法在屏幕上显示任何东西,也无法保存任何数据。我们的目标是,将副作用从我们的核心业务逻辑中分离出去,把它们推到程序的边缘

让我们重构上面的例子,让核心逻辑变纯:

javascript
const user = { name: 'Alice', age: 30 };

// 这是一个纯函数,它只负责计算新的年龄
// 它不修改原始对象,而是返回一个全新的对象
const haveBirthday = (person) => {
  return {
    ...person,
    age: person.age + 1
  };
};

// 副作用被隔离在这里
const new_user = haveBirthday(user);
console.log(`Happy birthday, ${new_user.name}!`); // 日志输出
saveToDatabase(new_user); // 数据库操作

console.log(user); // { name: 'Alice', age: 30 } -> 原始的 user 对象安然无恙!

看到了吗?haveBirthday 现在是一个纯函数。它接收一个对象,返回一个新的对象,原始的 user 对象保持不变。这种“不可变性”我们将在下一章深入探讨。

现在,我们的核心逻辑(年龄增长)是纯粹的、可预测的、易于测试的。而那些不可避免的副作用(打印日志、保存数据)则被明确地放在了程序的边界上执行。

纯函数的好处:为什么值得我们追求?

当你开始用纯函数的思维方式来构建代码时,你会发现它带来了巨大的好处:

  • 可预测性:代码的行为不再神秘莫测。给定相同的输入,你总能得到相同的输出,这让调试变得异常简单。
  • 易于测试:测试纯函数是你能想象到的最简单的事情。不需要复杂的 mockspy,只需提供输入,然后断言输出是否符合预期。
  • 可组合性:纯函数就像一个个独立的乐高积木。你可以放心地把它们组合起来,因为你知道它们不会相互干扰,从而构建出更强大的功能。
  • 并行处理:因为纯函数不依赖共享状态,所以它们非常适合并行和分布式计算,这在处理大规模数据时尤其重要。

在 Ramda 的世界里,几乎所有的函数都被设计成纯函数。这正是 Ramda 如此强大和可靠的根源。通过拥抱纯函数,我们正在为构建一个更健壮、更清晰的软件世界奠定基础。

2. 纯函数与副作用:构建可靠代码的基石 has loaded