纯函数(Pure Function)详解
定义
纯函数是一种函数,其输出只由输入决定,并且在运行过程中不产生任何副作用。这里的副作用指的是函数对外部环境的任何改变,包括但不限于改变全局变量、修改输入参数、执行I/O操作等。
纯函数需要满足以下条件:
- 给定相同的输入,总是返回相同的输出
- 没有副作用(Side Effects)
- 不依赖外部状态
代码示例
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 返回,不会修改外部变量。
分析
这个举例有一点不准确,让我解释一下:
-
map、filter、reduce 本身是纯函数,这点说得对。它们:
- 不依赖外部状态
- 相同输入得到相同输出
- 不会修改原数组
-
但回调函数不一定是纯函数,这才是关键。
代码示例
纯函数的例子
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 等数组方法本身是纯函数,它们不会修改原数组,而是返回新数组。但这些方法的最终表现取决于传入的回调函数是否纯函数。为了保持整体的纯函数特性,我们应该确保传入的回调函数也是纯函数。
实际应用建议
- 尽可能使用纯函数
- 将副作用隔离在特定的函数中
- 使用函数组合来构建复杂功能
- 对于不可避免的副作用,清晰地文档化
- 在React等框架中,尽量将组件写成纯函数
纯函数是函数式编程的基石,能够提高代码的可维护性、可测试性和可重用性。但在实际开发中,并非所有函数都能是纯函数,关键是要合理平衡使用。