JavaScript 纯函数
什么是纯函数?
在函数式编程中,纯函数是一个核心概念。简单来说,纯函数是一种特殊类型的函数,它具有两个主要特征:
- 确定性:给定相同的输入,总是返回相同的输出
- 无副作用:函数执行过程中不会修改外部状态或产生可观察的副作用
副作用包括:修改全局变量、修改传入的参数、进行I/O操作(如文件读写、网络请求)、DOM操作等。
纯函数的特点
1. 可预测性
纯函数的结果仅由其输入参数决定,不依赖于任何外部状态,这使得函数行为完全可预测。
// 纯函数
function add(a, b) {
return a + b;
}
// 调用多次,结果始终相同
console.log(add(2, 3)); // 输出: 5
console.log(add(2, 3)); // 输出: 5
2. 可测试性
由于纯函数的输出只依赖于输入,所以测试变得简单直接,不需要模拟复杂的环境。
3. 可缓存性
纯函数的结果可以被缓存,因为相同的输入总是产生相同的输出。
// 使用闭包实现一个简单的记忆化函数
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (!cache[key]) {
cache[key] = fn(...args);
}
return cache[key];
};
}
// 计算斐波那契数列的纯函数
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 记忆化版本
const memoFibonacci = memoize(fibonacci);
console.time('Without memoization');
fibonacci(35);
console.timeEnd('Without memoization'); // 显著较慢
console.time('With memoization');
memoFibonacci(35);
console.timeEnd('With memoization'); // 显著较快
4. 可并行执行
因为纯函数不依赖共享状态且没有副作用,所以多个纯函数可以安全地并行执行。
纯函数 vs 非纯函数
让我们通过对比来更好地理解纯函数:
// 纯函数
function calculateArea(radius) {
return Math.PI * radius * radius;
}
// 非纯函数 - 依赖外部变量
let PI = 3.14;
function calculateAreaImpure(radius) {
return PI * radius * radius;
}
// 非纯函数 - 修改外部状态
let counter = 0;
function incrementCounter() {
counter++;
return counter;
}
// 非纯函数 - 有副作用
function logMessage(message) {
console.log(message); // 副作用:控制台输出
return message;
}
如何编写纯函数
要编写纯函数,请遵循以下原则:
-
仅使用函数参数:函数内的计算应该只依赖于函数的参数
-
不修改输入参数:避免修改传入的对象或数组
// 不好的做法 - 修改输入参数
function addItem(arr, item) {
arr.push(item);
return arr;
}
// 好的做法 - 不修改输入参数
function addItem(arr, item) {
return [...arr, item];
}
-
避免共享状态:不依赖或修改函数外部的状态
-
避免副作用:不执行网络请求、文件操作、DOM操作等副作用
实际应用案例
1. 数据转换
纯函数非常适合用于数据转换逻辑:
// 将用户数据转换为显示格式
function formatUser(user) {
return {
displayName: `${user.firstName} ${user.lastName}`,
fullAddress: `${user.address.street}, ${user.address.city}, ${user.address.country}`,
isAdmin: user.role === 'admin'
};
}
const user = {
firstName: 'John',
lastName: 'Doe',
role: 'user',
address: {
street: '123 Main St',
city: 'Boston',
country: 'USA'
}
};
const formattedUser = formatUser(user);
console.log(formattedUser);
/* 输出:
{
displayName: 'John Doe',
fullAddress: '123 Main St, Boston, USA',
isAdmin: false
}
*/
2. 筛选和排序
// 筛选活跃用户并按名字排序
function getActiveUsersSortedByName(users) {
return [...users]
.filter(user => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name));
}
const users = [
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false },
{ id: 3, name: 'Charlie', isActive: true }
];
const activeUsersSorted = getActiveUsersSortedByName(users);
console.log(activeUsersSorted);
/* 输出:
[
{ id: 1, name: 'Alice', isActive: true },
{ id: 3, name: 'Charlie', isActive: true }
]
*/
3. 状态计算
// 计算购物车总价
function calculateTotal(cartItems) {
return cartItems.reduce(
(total, item) => total + item.price * item.quantity,
0
);
}
const cart = [
{ id: 1, name: 'Book', price: 20, quantity: 2 },
{ id: 2, name: 'Pen', price: 5, quantity: 3 }
];
console.log(calculateTotal(cart)); // 输出: 55
纯函数的优势
1. 代码可维护性
纯函数使代码更容易理解和维护,因为它们的行为是隔离的,不会影响或依赖其他部分的代码。
2. 可复用性
纯函数可以在不同的上下文中安全地重用,不用担心状态污染。
3. 测试简单
测试纯函数非常直接,只需要验证给定输入是否产生预期输出,不需要模拟复杂的环境。
// 一个简单的测试示例
function add(a, b) {
return a + b;
}
// 测试
function testAdd() {
console.assert(add(2, 3) === 5, '2 + 3 should be 5');
console.assert(add(-1, 1) === 0, '-1 + 1 should be 0');
console.assert(add(0, 0) === 0, '0 + 0 should be 0');
console.log('All tests passed!');
}
testAdd();
何时避免使用纯函数
虽然纯函数有很多优势,但在某些情况下,我们不得不使用非纯函数:
- I/O操作:文件读写、网络请求、数据库操作等
- 用户交互:处理用户输入、更新UI等
- 时间相关操作:获取当前时间、生成随机数等
- 状态管理:某些情况下需要维护和更新应用状态
将非纯函数转换为纯函数
有时,我们可以通过依赖注入将非纯函数转换为纯函数:
// 非纯函数 - 依赖当前时间
function greet() {
const hour = new Date().getHours();
return hour < 12 ? 'Good morning!' : 'Good afternoon!';
}
// 改进为纯函数 - 通过参数注入依赖
function pureGreet(hour) {
return hour < 12 ? 'Good morning!' : 'Good afternoon!';
}
// 使用方式
const currentHour = new Date().getHours();
console.log(pureGreet(currentHour));
总结
纯函数是函数式编程的基石,它们具有可预测性、可测试性、可缓存性和可并行性等优点。通过编写纯函数,我们可以使代码更加健壮、可维护,并减少错误。
尽管在实际应用中,我们无法完全避免非纯函数(特别是涉及I/O操作时),但应该尽可能地隔离这些非纯部分,让大部分代码保持纯净。
练习
为了加深对纯函数的理解,尝试完成以下练习:
-
将下面的非纯函数转换为纯函数:
javascriptlet total = 0;
function addToTotal(value) {
total += value;
return total;
} -
编写一个纯函数,接收一个用户对象数组,返回年龄大于18且已激活的用户数量。
-
改造以下函数,使其成为纯函数:
javascriptfunction processOrder(order) {
order.status = 'processed';
order.processedAt = new Date();
return order;
}
附加资源
要进一步学习纯函数和函数式编程,可以参考以下资源: