跳到主要内容

JavaScript 纯函数

什么是纯函数?

在函数式编程中,纯函数是一个核心概念。简单来说,纯函数是一种特殊类型的函数,它具有两个主要特征:

  1. 确定性:给定相同的输入,总是返回相同的输出
  2. 无副作用:函数执行过程中不会修改外部状态或产生可观察的副作用
备注

副作用包括:修改全局变量、修改传入的参数、进行I/O操作(如文件读写、网络请求)、DOM操作等。

纯函数的特点

1. 可预测性

纯函数的结果仅由其输入参数决定,不依赖于任何外部状态,这使得函数行为完全可预测。

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

// 调用多次,结果始终相同
console.log(add(2, 3)); // 输出: 5
console.log(add(2, 3)); // 输出: 5

2. 可测试性

由于纯函数的输出只依赖于输入,所以测试变得简单直接,不需要模拟复杂的环境。

3. 可缓存性

纯函数的结果可以被缓存,因为相同的输入总是产生相同的输出。

javascript
// 使用闭包实现一个简单的记忆化函数
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 非纯函数

让我们通过对比来更好地理解纯函数:

javascript
// 纯函数
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;
}

如何编写纯函数

要编写纯函数,请遵循以下原则:

  1. 仅使用函数参数:函数内的计算应该只依赖于函数的参数

  2. 不修改输入参数:避免修改传入的对象或数组

javascript
// 不好的做法 - 修改输入参数
function addItem(arr, item) {
arr.push(item);
return arr;
}

// 好的做法 - 不修改输入参数
function addItem(arr, item) {
return [...arr, item];
}
  1. 避免共享状态:不依赖或修改函数外部的状态

  2. 避免副作用:不执行网络请求、文件操作、DOM操作等副作用

实际应用案例

1. 数据转换

纯函数非常适合用于数据转换逻辑:

javascript
// 将用户数据转换为显示格式
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. 筛选和排序

javascript
// 筛选活跃用户并按名字排序
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. 状态计算

javascript
// 计算购物车总价
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. 测试简单

测试纯函数非常直接,只需要验证给定输入是否产生预期输出,不需要模拟复杂的环境。

javascript
// 一个简单的测试示例
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();

何时避免使用纯函数

虽然纯函数有很多优势,但在某些情况下,我们不得不使用非纯函数:

  1. I/O操作:文件读写、网络请求、数据库操作等
  2. 用户交互:处理用户输入、更新UI等
  3. 时间相关操作:获取当前时间、生成随机数等
  4. 状态管理:某些情况下需要维护和更新应用状态

将非纯函数转换为纯函数

有时,我们可以通过依赖注入将非纯函数转换为纯函数:

javascript
// 非纯函数 - 依赖当前时间
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操作时),但应该尽可能地隔离这些非纯部分,让大部分代码保持纯净。

练习

为了加深对纯函数的理解,尝试完成以下练习:

  1. 将下面的非纯函数转换为纯函数:

    javascript
    let total = 0;

    function addToTotal(value) {
    total += value;
    return total;
    }
  2. 编写一个纯函数,接收一个用户对象数组,返回年龄大于18且已激活的用户数量。

  3. 改造以下函数,使其成为纯函数:

    javascript
    function processOrder(order) {
    order.status = 'processed';
    order.processedAt = new Date();
    return order;
    }

附加资源

要进一步学习纯函数和函数式编程,可以参考以下资源: