JS 纯函数:提升代码可测试性、可维护性与可预测性

各位观众老爷,大家好!今天咱们聊聊JavaScript里的“纯函数”这玩意儿。别看它名字听着像高冷女神,其实是个务实居家型的好伙伴,能让你的代码变得更靠谱、更易懂、更方便“调戏”(测试)。

开场白:为什么要搞“纯”?

想象一下,你辛辛苦苦写了一段代码,结果每次运行出来的结果都不一样,就像抽奖一样,全凭运气。这酸爽,谁用谁知道!这就是“非纯函数”的威力,它们会偷偷摸摸地修改外部世界,让你防不胜防。

而“纯函数”就不一样了,它们就像老实人,只干自己分内的事,不搞小动作,给你稳定的预期。用人话说,就是:

  • 相同的输入,永远得到相同的输出。
  • 没有任何副作用。

啥叫“副作用”?

“副作用”就是指函数在执行过程中,除了返回结果之外,还对外部环境产生了影响。比如:

  • 修改了全局变量
  • 修改了函数参数
  • 进行了 I/O 操作(比如读写文件、网络请求)
  • 操作了 DOM

这些行为都可能导致你的代码变得难以预测和调试。

纯函数的好处:简直不要太多!

  • 可测试性高: 因为输入和输出是确定的,所以你可以很容易地编写单元测试来验证函数的正确性。
  • 可维护性强: 函数之间相互独立,修改一个函数不会影响到其他函数,降低了维护成本。
  • 可预测性好: 相同的输入总是得到相同的输出,更容易理解和调试代码。
  • 易于复用: 纯函数只依赖于输入参数,可以在不同的场景下复用。
  • 并发安全: 由于没有副作用,纯函数可以在并发环境中安全地执行。
  • 方便缓存: 因为结果只依赖于输入,所以可以对纯函数的结果进行缓存,提高性能(这就是所谓的“memoization”)。

纯函数 VS 非纯函数:举个栗子

我们先来个简单的例子,对比一下纯函数和非纯函数的区别:

// 非纯函数:修改了全局变量
let counter = 0;
function incrementCounter() {
  counter++;
  return counter;
}

console.log(incrementCounter()); // 输出 1
console.log(incrementCounter()); // 输出 2
console.log(counter); // 输出 2

// 纯函数:不修改外部状态
function add(a, b) {
  return a + b;
}

console.log(add(2, 3)); // 输出 5
console.log(add(2, 3)); // 输出 5
console.log(counter); // 输出 2 (counter 的值没有改变)

在这个例子中,incrementCounter 是一个非纯函数,因为它修改了全局变量 counter。每次调用 incrementCountercounter 的值都会增加,导致输出结果不确定。而 add 是一个纯函数,它只依赖于输入参数 ab,并且没有副作用,所以每次调用 add(2, 3) 都会得到相同的结果 5

再来点复杂的:数组操作

数组操作是 JavaScript 中常见的任务,我们来看看如何用纯函数的方式来处理数组:

// 非纯函数:修改了原数组
function addItem(arr, item) {
  arr.push(item);
  return arr;
}

let myArray = [1, 2, 3];
let newArray = addItem(myArray, 4);

console.log(myArray);   // 输出 [1, 2, 3, 4] (原数组被修改了)
console.log(newArray);  // 输出 [1, 2, 3, 4]

// 纯函数:不修改原数组,返回一个新数组
function addItemPure(arr, item) {
  return [...arr, item]; // 使用扩展运算符创建新数组
}

let myArrayPure = [1, 2, 3];
let newArrayPure = addItemPure(myArrayPure, 4);

console.log(myArrayPure);    // 输出 [1, 2, 3] (原数组没有被修改)
console.log(newArrayPure);   // 输出 [1, 2, 3, 4]

addItem 直接修改了原数组 myArray,这是副作用。而 addItemPure 使用了扩展运算符 ... 创建了一个新的数组,并将 item 添加到新数组中,原数组 myArrayPure 没有被修改,这才是纯函数该有的样子。

常用的纯函数技巧:

  • 使用 mapfilterreduce 等数组方法: 这些方法都是纯函数,它们不会修改原数组,而是返回一个新的数组。
  • 使用扩展运算符 ... 创建新数组或新对象: 避免直接修改原有的数据结构。
  • 避免修改函数参数: 将函数参数视为只读的。
  • 尽量使用局部变量: 避免使用全局变量,减少副作用。

表格总结:纯函数 vs 非纯函数

特性 纯函数 非纯函数
输入 相同的输入 相同的输入
输出 永远得到相同的输出 可能得到不同的输出
副作用 没有副作用 存在副作用
可测试性
可维护性
可预测性
并发安全 安全 不安全
缓存 方便缓存 (memoization) 难以缓存
例子 add(a, b) => a + b incrementCounter() { counter++; }
数组操作 [...arr, item] (创建新数组) arr.push(item) (修改原数组)

代码示例:一个稍微复杂点的例子(购物车计算)

假设我们要计算一个购物车的总价,可以这样写:

// 购物车数据 (商品ID, 价格, 数量)
const cart = [
  { id: 1, price: 10, quantity: 2 },
  { id: 2, price: 20, quantity: 1 },
  { id: 3, price: 5, quantity: 4 },
];

// 纯函数:计算单个商品的总价
function calculateItemTotal(item) {
  return item.price * item.quantity;
}

// 纯函数:计算购物车总价
function calculateCartTotal(cart) {
  return cart.reduce((total, item) => total + calculateItemTotal(item), 0);
}

const total = calculateCartTotal(cart);
console.log("购物车总价:", total); // 输出 购物车总价: 60

在这个例子中,calculateItemTotalcalculateCartTotal 都是纯函数。它们只依赖于输入参数,并且没有副作用,可以很容易地进行测试和复用。

关于副作用的“控制”

虽然我们推崇纯函数,但这并不意味着要完全消除副作用。在实际开发中,有些操作是不可避免的,比如 I/O 操作、DOM 操作等。关键在于我们要控制副作用,尽量将它们隔离在特定的模块或函数中,使其影响范围最小化。

例如,可以将 UI 渲染操作放在一个单独的函数中,这个函数可能不是纯函数,但它可以将副作用限制在 UI 层面,而不会影响到其他模块的逻辑。

函数式编程:纯函数的最佳搭档

纯函数是函数式编程的核心概念之一。函数式编程强调使用纯函数来构建应用程序,通过组合纯函数来实现复杂的功能。函数式编程可以提高代码的可读性、可维护性和可测试性,使代码更加健壮。

一些小提示:

  • 代码审查: 在代码审查过程中,重点关注函数是否是纯函数,是否有副作用。
  • 重构: 如果发现代码中存在非纯函数,尝试将其重构为纯函数。
  • 工具: 可以使用一些工具来辅助检查代码中的副作用,比如 ESLint。

最后总结

纯函数是 JavaScript 中一种非常有用的编程技巧,它可以提高代码的可测试性、可维护性和可预测性。虽然不能完全避免副作用,但我们可以通过控制副作用,尽量使用纯函数来构建应用程序。拥抱纯函数,让你的代码更上一层楼!

感谢各位的观看,希望今天的分享对大家有所帮助!下次有机会再和大家聊聊其他有趣的编程话题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注