什么是纯函数?

211 阅读3分钟

纯函数(Pure Function)详解

定义

纯函数是一种函数,其输出只由输入决定,并且在运行过程中不产生任何副作用。这里的副作用指的是函数对外部环境的任何改变,包括但不限于改变全局变量、修改输入参数、执行I/O操作等。

纯函数需要满足以下条件:

  1. 给定相同的输入,总是返回相同的输出
  2. 没有副作用(Side Effects)
  3. 不依赖外部状态

代码示例

1. 纯函数示例

// 纯函数
function add(a, b) {
  return a + b;
}

// 纯函数
function calculateTotal(items) {
  return items.reduce((total, item) => total + item.price, 0);
}

2. 非纯函数示例

// 非纯函数:依赖外部变量
let tax = 0.1;
function calculateTax(price) {
  return price * tax; // 依赖外部变量tax
}

// 非纯函数:修改外部状态
let counter = 0;
function increment() {
  counter++; // 修改外部状态
  return counter;
}

// 非纯函数:有副作用
function saveUser(user) {
  localStorage.setItem('user', JSON.stringify(user)); // 副作用:操作localStorage
  return user;
}

纯函数的优势

1. 可测试性

// 纯函数易于测试
function multiply(a, b) {
  return a * b;
}

// 测试用例
test('multiply', () => {
  expect(multiply(2, 3)).toBe(6);
  expect(multiply(0, 5)).toBe(0);
  expect(multiply(-2, 3)).toBe(-6);
});

2. 可缓存性

// 使用记忆化缓存纯函数的结果
const memoize = (fn) => {
  const cache = new Map();
  
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

3. 并行执行

// 纯函数可以安全地并行执行
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);

如何将非纯函数改写为纯函数

1. 依赖注入

// 非纯函数
function fetchUser(id) {
  return fetch(`/api/users/${id}`);
}

// 改写为纯函数
function fetchUser(api, id) {
  return api.fetch(`/api/users/${id}`);
}

2. 状态提升

// 非纯函数
let count = 0;
function increment() {
  count++;
  return count;
}

// 改写为纯函数
function increment(count) {
  return count + 1;
}

3. 返回新对象而不是修改原对象

// 非纯函数
function addItem(cart, item) {
  cart.items.push(item); // 修改原对象
  return cart;
}

// 改写为纯函数
function addItem(cart, item) {
  return {
    ...cart,
    items: [...cart.items, item] // 返回新对象
  };
}

关于数组方法是否为纯函数的分析

原始描述

比如:数组 map, filter, reduce 等方法,都是纯函数。因为这些方法都是基于数组本身进行操作,不会依赖外部变量,也不会修改外部变量。处理结果通过 return 返回,不会修改外部变量。

分析

这个举例有一点不准确,让我解释一下:

  1. map、filter、reduce 本身是纯函数,这点说得对。它们:

    • 不依赖外部状态
    • 相同输入得到相同输出
    • 不会修改原数组
  2. 但回调函数不一定是纯函数,这才是关键。

代码示例

纯函数的例子

const numbers = [1, 2, 3];

// ✅ 纯函数的使用
const doubled = numbers.map(x => x * 2);
const evenNumbers = numbers.filter(x => x % 2 === 0);
const sum = numbers.reduce((acc, curr) => acc + curr, 0);

非纯函数的例子

// ❌ 非纯函数的使用
let multiplier = 2;
const result = numbers.map(x => x * multiplier); // 依赖外部变量

let sideEffectSum = 0;
numbers.forEach(x => {
  sideEffectSum += x; // 产生副作用
});

const dates = ['2023-01-01', '2023-01-02'];
const randomDates = dates.map(d => {
  return new Date(d).getTime() + Math.random(); // 每次结果不同
});

正确的说法

map、filter、reduce 等数组方法本身是纯函数,它们不会修改原数组,而是返回新数组。但这些方法的最终表现取决于传入的回调函数是否纯函数。为了保持整体的纯函数特性,我们应该确保传入的回调函数也是纯函数。

实际应用建议

  1. 尽可能使用纯函数
  2. 将副作用隔离在特定的函数中
  3. 使用函数组合来构建复杂功能
  4. 对于不可避免的副作用,清晰地文档化
  5. 在React等框架中,尽量将组件写成纯函数

纯函数是函数式编程的基石,能够提高代码的可维护性、可测试性和可重用性。但在实际开发中,并非所有函数都能是纯函数,关键是要合理平衡使用。