Skip to content

纯函数:可预测且可靠的代码基石

在数学课上,你可能记得这样的函数:f(x) = 2x + 1。无论何时何地,只要输入是 3,输出永远是 7。这个函数不会因为今天是星期几、你在哪里计算、或者你之前输入过什么而改变结果。这种可预测性正是纯函数的核心特征。在编程世界中,纯函数就像这样的数学函数——给定相同的输入,永远返回相同的输出,并且不会影响外部世界。

什么是纯函数

纯函数(Pure Function)是函数式编程的核心概念,它必须同时满足两个关键条件:

第一,相同的输入总是产生相同的输出。 这意味着函数的返回值只依赖于输入参数,不依赖任何外部状态。无论调用多少次,只要参数相同,结果就相同。

第二,不产生副作用。 函数执行过程中不会修改外部状态,不会改变传入的参数对象,不会进行 I/O 操作,不会修改全局变量。函数就像一个封闭的黑盒,只接收输入,产生输出,但不会影响外部世界。

让我们看一个简单的纯函数示例:

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

console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5 - 永远返回相同结果
console.log(add(2, 3)); // 5 - 无论调用多少次

这个 add 函数是一个纯函数。它的返回值只依赖于参数 ab,每次用相同的参数调用都会得到相同的结果。它不会修改任何外部变量,不会产生任何副作用。

相比之下,这是一个不纯的函数:

javascript
// ❌ 非纯函数:依赖外部变量
let multiplier = 2;

function multiply(number) {
  return number * multiplier;
}

console.log(multiply(5)); // 10

multiplier = 3;
console.log(multiply(5)); // 15 - 相同输入,不同输出

这个函数不是纯函数,因为它的返回值依赖于外部变量 multiplier。当 multiplier 的值改变时,即使输入相同,输出也会不同。函数的行为变得不可预测。

纯函数的两个核心特性

1. 引用透明性(Referential Transparency)

纯函数具有引用透明性,这意味着你可以用函数的返回值直接替换函数调用,程序的行为不会改变。这个特性让代码更容易理解和重构。

javascript
// 纯函数
function square(x) {
  return x * x;
}

function sumOfSquares(a, b) {
  return square(a) + square(b);
}

console.log(sumOfSquares(3, 4)); // 25

// 由于引用透明性,可以直接替换
console.log(9 + 16); // 25 - 行为完全相同

因为 square(3) 总是返回 9,square(4) 总是返回 16,我们可以直接用这些值替换函数调用,程序的行为保持不变。这种特性让编译器和开发者都能更容易地优化和理解代码。

2. 无副作用(No Side Effects)

纯函数不会产生任何可观察的副作用。常见的副作用包括:

  • 修改全局变量或外部作用域的变量
  • 修改传入的参数对象
  • 执行 I/O 操作(读写文件、网络请求、控制台输出)
  • 修改 DOM
  • 调用不纯的函数
  • 生成随机数或获取当前时间

让我们通过对比来理解副作用:

javascript
// ❌ 有副作用:修改外部变量
let total = 0;

function addToTotal(value) {
  total += value; // 修改了外部变量
  return total;
}

console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 - 相同输入,不同输出
console.log(total); // 10 - 外部状态被改变

// ✅ 无副作用:纯函数实现
function calculateNewTotal(currentTotal, value) {
  return currentTotal + value; // 只返回新值,不修改任何东西
}

let myTotal = 0;
myTotal = calculateNewTotal(myTotal, 5);
console.log(myTotal); // 5
myTotal = calculateNewTotal(myTotal, 5);
console.log(myTotal); // 10

纯函数版本不会修改任何外部状态。它接收当前总数和要添加的值作为参数,返回新的总数。虽然我们仍然需要管理状态(myTotal),但这个管理是显式的、可控的。

识别纯函数与非纯函数

理解如何识别纯函数对编写高质量代码至关重要。让我们通过更多示例来练习识别:

纯函数示例

javascript
// ✅ 纯函数:字符串处理
function getFullName(firstName, lastName) {
  return `${firstName} ${lastName}`;
}

console.log(getFullName("John", "Doe")); // John Doe
console.log(getFullName("John", "Doe")); // John Doe - 可预测

// ✅ 纯函数:数组操作(不修改原数组)
function doubleNumbers(numbers) {
  return numbers.map((num) => num * 2);
}

let original = [1, 2, 3];
let doubled = doubleNumbers(original);
console.log(doubled); // [2, 4, 6]
console.log(original); // [1, 2, 3] - 原数组未被修改

// ✅ 纯函数:对象操作(不修改原对象)
function updateUserAge(user, newAge) {
  return {
    ...user,
    age: newAge,
  };
}

let user = { name: "Alice", age: 25 };
let updatedUser = updateUserAge(user, 26);
console.log(updatedUser); // { name: "Alice", age: 26 }
console.log(user); // { name: "Alice", age: 25 } - 原对象未被修改

// ✅ 纯函数:条件逻辑
function getDiscountedPrice(price, isPremium) {
  return isPremium ? price * 0.8 : price;
}

console.log(getDiscountedPrice(100, true)); // 80
console.log(getDiscountedPrice(100, false)); // 100

这些函数都是纯函数,因为它们:

  • 输出只依赖于输入参数
  • 不修改任何外部状态
  • 不修改传入的参数
  • 具有可预测性

非纯函数示例

javascript
// ❌ 非纯函数:依赖外部状态
let discount = 0.1;

function applyDiscount(price) {
  return price * (1 - discount); // 依赖外部变量
}

// ❌ 非纯函数:修改传入的参数
function addItemToCart(cart, item) {
  cart.push(item); // 修改了传入的数组
  return cart;
}

// ❌ 非纯函数:使用随机数
function generateRandomId() {
  return Math.random().toString(36).substr(2, 9); // 每次返回不同结果
}

// ❌ 非纯函数:依赖当前时间
function isBusinessHours() {
  let hour = new Date().getHours(); // 依赖当前时间
  return hour >= 9 && hour < 17;
}

// ❌ 非纯函数:执行 I/O 操作
function logAndReturn(value) {
  console.log(value); // 控制台输出是副作用
  return value;
}

// ❌ 非纯函数:修改 DOM
function updatePageTitle(title) {
  document.title = title; // 修改DOM是副作用
  return title;
}

这些函数都不是纯函数,因为它们要么依赖外部状态,要么产生了副作用,要么两者兼而有之。

将非纯函数转换为纯函数

很多时候,我们可以通过重新设计来将非纯函数转换为纯函数。关键思路是将外部依赖作为参数传入,将副作用移到函数外部。

示例 1:消除外部依赖

javascript
// ❌ 非纯:依赖外部配置
let taxRate = 0.08;

function calculateTotal(price) {
  return price * (1 + taxRate);
}

// ✅ 纯函数:将外部依赖作为参数
function calculatePureTotal(price, taxRate) {
  return price * (1 + taxRate);
}

// 使用时显式传入配置
let total = calculatePureTotal(100, 0.08);
console.log(total); // 108

通过将 taxRate 作为参数传入,函数变成了纯函数。现在它的行为完全由输入决定,不依赖任何外部状态。

示例 2:避免修改参数

javascript
// ❌ 非纯:修改传入的对象
function incrementAge(person) {
  person.age++;
  return person;
}

let alice = { name: "Alice", age: 25 };
incrementAge(alice);
console.log(alice.age); // 26 - 原对象被修改

// ✅ 纯函数:返回新对象
function createOlderPerson(person) {
  return {
    ...person,
    age: person.age + 1,
  };
}

let bob = { name: "Bob", age: 30 };
let olderBob = createOlderPerson(bob);
console.log(bob.age); // 30 - 原对象未被修改
console.log(olderBob.age); // 31 - 新对象包含更新的年龄

纯函数版本使用展开运算符创建了一个新对象,而不是修改原对象。这确保了函数没有副作用。

示例 3:处理数组操作

javascript
// ❌ 非纯:修改原数组
function addNumber(numbers, newNumber) {
  numbers.push(newNumber); // 修改了原数组
  return numbers;
}

// ✅ 纯函数:返回新数组
function addNumberPure(numbers, newNumber) {
  return [...numbers, newNumber];
}

let originalNumbers = [1, 2, 3];
let newNumbers = addNumberPure(originalNumbers, 4);
console.log(originalNumbers); // [1, 2, 3] - 未被修改
console.log(newNumbers); // [1, 2, 3, 4] - 新数组

// ✅ 纯函数:移除元素
function removeItem(array, index) {
  return array.filter((_, i) => i !== index);
}

let fruits = ["apple", "banana", "orange"];
let remaining = removeItem(fruits, 1);
console.log(fruits); // ["apple", "banana", "orange"] - 原数组未变
console.log(remaining); // ["apple", "orange"] - 新数组

示例 4:隔离时间依赖

javascript
// ❌ 非纯:依赖当前时间
function getGreeting() {
  let hour = new Date().getHours();
  if (hour < 12) return "Good morning";
  if (hour < 18) return "Good afternoon";
  return "Good evening";
}

// ✅ 纯函数:接收时间作为参数
function getGreetingPure(hour) {
  if (hour < 12) return "Good morning";
  if (hour < 18) return "Good afternoon";
  return "Good evening";
}

// 在调用时传入当前时间(副作用在外部处理)
let currentHour = new Date().getHours();
let greeting = getGreetingPure(currentHour);
console.log(greeting);

通过将时间作为参数传入,函数本身变成了纯函数。获取当前时间的副作用被移到了函数外部,使函数更容易测试和理解。

纯函数的优势

1. 易于测试

纯函数是最容易测试的代码类型。因为输出只依赖输入,你不需要设置复杂的测试环境或模拟外部状态。

javascript
// 纯函数
function calculateShippingCost(weight, distance) {
  let baseCost = 5;
  let weightCost = weight * 0.5;
  let distanceCost = distance * 0.1;
  return baseCost + weightCost + distanceCost;
}

// 测试非常简单
console.log(calculateShippingCost(10, 100) === 20); // true
console.log(calculateShippingCost(5, 50) === 12.5); // true
console.log(calculateShippingCost(0, 0) === 5); // true

你可以直接调用函数并验证结果,不需要任何设置或清理工作。测试是确定性的,每次运行都会得到相同的结果。

2. 可预测性

纯函数的行为完全可预测。给定相同的输入,你总是知道会得到什么输出。这让代码更容易理解和调试。

javascript
// 可预测的纯函数
function formatPrice(amount, currency) {
  let symbols = {
    USD: "$",
    EUR: "€",
    GBP: "£",
  };
  let symbol = symbols[currency] || currency;
  return `${symbol}${amount.toFixed(2)}`;
}

// 行为完全可预测
console.log(formatPrice(99.99, "USD")); // $99.99
console.log(formatPrice(99.99, "EUR")); // €99.99
console.log(formatPrice(99.99, "JPY")); // JPY99.99

无论何时何地调用这个函数,只要参数相同,结果就相同。这种可预测性让代码更可靠。

3. 可缓存性(Memoization)

因为纯函数对于相同输入总是返回相同输出,我们可以缓存函数的结果来优化性能。这种技术称为记忆化(memoization)。

javascript
// 创建一个记忆化的包装器
function memoize(fn) {
  let cache = new Map();

  return function (...args) {
    let key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log("Returning cached result");
      return cache.get(key);
    }

    console.log("Computing result");
    let result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// 纯函数:计算斐波那契数(简化版)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 使用记忆化优化
let memoizedFib = memoize(fibonacci);

console.log(memoizedFib(5)); // Computing result -> 5
console.log(memoizedFib(5)); // Returning cached result -> 5
console.log(memoizedFib(6)); // Computing result -> 8

这种优化只对纯函数有效。如果函数有副作用或依赖外部状态,缓存就会产生错误的结果。

4. 并发安全

纯函数不依赖或修改共享状态,因此它们天生是线程安全的。多个函数可以并发执行而不会互相干扰。

javascript
// 纯函数可以安全地并发执行
function processUserData(user) {
  return {
    id: user.id,
    fullName: `${user.firstName} ${user.lastName}`.toUpperCase(),
    email: user.email.toLowerCase(),
    age: new Date().getFullYear() - user.birthYear,
  };
}

// 可以并行处理多个用户,不会有竞态条件
let users = [
  {
    id: 1,
    firstName: "John",
    lastName: "Doe",
    email: "[email protected]",
    birthYear: 1990,
  },
  {
    id: 2,
    firstName: "Jane",
    lastName: "Smith",
    email: "[email protected]",
    birthYear: 1985,
  },
];

let processedUsers = users.map(processUserData);
console.log(processedUsers);
// [
//   { id: 1, fullName: "JOHN DOE", email: "[email protected]", age: 35 },
//   { id: 2, fullName: "JANE SMITH", email: "[email protected]", age: 40 }
// ]

5. 易于组合

纯函数可以轻松组合成更复杂的函数。因为每个函数都是独立的、可预测的,将它们组合在一起不会产生意外的行为。

javascript
// 简单的纯函数
function double(x) {
  return x * 2;
}

function addOne(x) {
  return x + 1;
}

function square(x) {
  return x * x;
}

// 组合函数
function compose(...fns) {
  return function (value) {
    return fns.reduceRight((acc, fn) => fn(acc), value);
  };
}

// 创建组合函数
let doubleThenAddOne = compose(addOne, double);
let squareThenDouble = compose(double, square);

console.log(doubleThenAddOne(5)); // 11 - (5 * 2) + 1
console.log(squareThenDouble(3)); // 18 - (3 * 3) * 2

实际应用场景

1. 数据转换管道

纯函数非常适合构建数据转换管道,每个步骤都是独立、可测试的。

javascript
// 一系列纯函数处理用户数据
function validateUser(user) {
  return {
    ...user,
    isValid: Boolean(user.email && user.name && user.age >= 18),
  };
}

function normalizeEmail(user) {
  return {
    ...user,
    email: user.email.toLowerCase().trim(),
  };
}

function addMembershipLevel(user) {
  let level = user.age < 25 ? "junior" : user.age < 60 ? "standard" : "senior";
  return { ...user, membershipLevel: level };
}

function addFullName(user) {
  return {
    ...user,
    fullName: `${user.firstName} ${user.lastName}`,
  };
}

// 组合成处理管道
function processUser(user) {
  return addFullName(addMembershipLevel(normalizeEmail(validateUser(user))));
}

let rawUser = {
  firstName: "Alice",
  lastName: "Johnson",
  email: "  [email protected]  ",
  age: 28,
};

let processedUser = processUser(rawUser);
console.log(processedUser);
// {
//   firstName: "Alice",
//   lastName: "Johnson",
//   email: "[email protected]",
//   age: 28,
//   isValid: true,
//   membershipLevel: "standard",
//   fullName: "Alice Johnson"
// }

2. 状态更新(React/Redux 风格)

在现代前端框架中,纯函数被广泛用于状态更新逻辑。

javascript
// Redux风格的纯函数reducer
function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM":
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price,
      };

    case "REMOVE_ITEM":
      let itemToRemove = state.items[action.payload.index];
      return {
        ...state,
        items: state.items.filter((_, i) => i !== action.payload.index),
        total: state.total - itemToRemove.price,
      };

    case "CLEAR_CART":
      return {
        items: [],
        total: 0,
      };

    default:
      return state;
  }
}

// 使用
let initialState = { items: [], total: 0 };

let state1 = cartReducer(initialState, {
  type: "ADD_ITEM",
  payload: { name: "Book", price: 20 },
});

let state2 = cartReducer(state1, {
  type: "ADD_ITEM",
  payload: { name: "Pen", price: 5 },
});

console.log(state2);
// {
//   items: [
//     { name: "Book", price: 20 },
//     { name: "Pen", price: 5 }
//   ],
//   total: 25
// }

console.log(initialState); // { items: [], total: 0 } - 原始状态未被修改

3. 数据验证

纯函数使数据验证逻辑清晰、可测试。

javascript
// 纯函数验证器
function isValidEmail(email) {
  let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function isValidAge(age) {
  return typeof age === "number" && age >= 0 && age <= 150;
}

function isStrongPassword(password) {
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password)
  );
}

// 组合验证函数
function validateRegistration(data) {
  return {
    emailValid: isValidEmail(data.email),
    ageValid: isValidAge(data.age),
    passwordValid: isStrongPassword(data.password),
    get isValid() {
      return this.emailValid && this.ageValid && this.passwordValid;
    },
  };
}

let registrationData = {
  email: "[email protected]",
  age: 25,
  password: "Secure123",
};

let validation = validateRegistration(registrationData);
console.log(validation);
// {
//   emailValid: true,
//   ageValid: true,
//   passwordValid: true,
//   isValid: true
// }

常见陷阱与注意事项

1. 对象和数组的浅拷贝陷阱

使用展开运算符只进行浅拷贝,嵌套对象可能仍然被修改。

javascript
// ❌ 浅拷贝的陷阱
function updateUserCity(user, newCity) {
  return {
    ...user, // 浅拷贝
    address: {
      ...user.address,
      city: newCity,
    },
  };
}

let user = {
  name: "John",
  address: {
    city: "New York",
    street: "5th Avenue",
  },
};

let updatedUser = updateUserCity(user, "Boston");
console.log(user.address.city); // New York - 正确,未被修改
console.log(updatedUser.address.city); // Boston

// ❌ 错误的做法(会修改原对象)
function updateUserCityWrong(user, newCity) {
  let newUser = { ...user };
  newUser.address.city = newCity; // 修改了共享的嵌套对象!
  return newUser;
}

let user2 = {
  name: "Jane",
  address: {
    city: "Paris",
    street: "Champs-Élysées",
  },
};

let updatedUser2 = updateUserCityWrong(user2, "London");
console.log(user2.address.city); // London - 糟糕!原对象被修改了

2. 数组方法的选择

某些数组方法会修改原数组,要小心选择。

javascript
// ❌ 会修改原数组的方法
let numbers = [3, 1, 4, 1, 5];
numbers.sort(); // 修改原数组
numbers.push(9); // 修改原数组
numbers.pop(); // 修改原数组
numbers.splice(1, 1); // 修改原数组

// ✅ 不修改原数组的方法(返回新数组)
let original = [3, 1, 4, 1, 5];
let sorted = [...original].sort(); // 先拷贝再排序
let filtered = original.filter((n) => n > 2); // 返回新数组
let mapped = original.map((n) => n * 2); // 返回新数组
let sliced = original.slice(1, 3); // 返回新数组

console.log(original); // [3, 1, 4, 1, 5] - 未被修改

3. 性能考量

频繁创建新对象和数组可能影响性能。在性能关键的场景中,需要权衡纯函数的优势和性能成本。

javascript
// 对于小数据量,纯函数方式性能良好
function addItem(list, item) {
  return [...list, item];
}

// 对于大数组,频繁复制可能影响性能
let largeArray = new Array(10000).fill(0);

// ❌ 性能较差:每次都复制整个数组
for (let i = 0; i < 1000; i++) {
  largeArray = addItem(largeArray, i);
}

// ✅ 在某些情况下,可以考虑变异方式(权衡取舍)
// 或者使用专门的不可变数据结构库(如 Immutable.js)

4. 并非所有代码都需要是纯函数

虽然纯函数有很多优势,但并非所有代码都应该是纯函数。I/O 操作、DOM 操作、API 调用等本质上就是副作用。

javascript
// ✅ 纯函数:处理逻辑
function prepareUserData(formData) {
  return {
    username: formData.username.trim().toLowerCase(),
    email: formData.email.trim().toLowerCase(),
    timestamp: Date.now(),
  };
}

// ❌ 必须有副作用:保存数据
async function saveUser(userData) {
  // API 调用是副作用,但这是必需的
  let response = await fetch("/api/users", {
    method: "POST",
    body: JSON.stringify(userData),
  });
  return response.json();
}

// 组合使用:纯函数处理数据,非纯函数执行副作用
async function registerUser(formData) {
  let userData = prepareUserData(formData); // 纯函数
  let result = await saveUser(userData); // 副作用
  return result;
}

关键是要明确区分纯函数和非纯函数,将副作用隔离到特定的地方,而将大部分逻辑保持为纯函数。

总结

纯函数是编写可靠、可维护代码的强大工具。它们就像数学函数一样可预测、可靠,为复杂的程序提供了坚实的基础。

关键要点:

  • 纯函数满足两个条件:相同输入产生相同输出,且无副作用
  • 纯函数具有引用透明性,可以用返回值直接替换函数调用
  • 纯函数的优势包括:易于测试、可预测、可缓存、并发安全、易于组合
  • 避免修改外部变量、传入参数、或执行 I/O 操作
  • 使用展开运算符、mapfilter 等方法创建新数据而非修改原数据
  • 注意嵌套对象的深拷贝问题
  • 在性能关键场景权衡纯函数的优势和成本
  • 并非所有代码都需要是纯函数,关键是隔离副作用

掌握纯函数的概念将帮助你编写更健壮的代码,也为学习函数式编程的其他概念(如函数柯里化和函数组合)打下基础。在实际开发中,尽可能多地使用纯函数,将副作用隔离到边界,你会发现代码变得更容易理解、测试和维护。