纯函数(Pure Functions)
纯函数是函数式编程的核心概念之一,在前端开发中扮演着至关重要的角色。理解并正确使用纯函数,能够显著提升代码质量、可维护性和应用性能
定义
纯函数是指满足以下两个条件的函数:
- 相同的输入永远得到相同的输出(确定性)
- 不产生任何副作用(无副作用性)
换句话说,纯函数就像一个数学函数:给定相同的参数,总是返回相同的结果,而且不会对程序的外部状态造成任何影响
javascript
// 纯函数示例
function add(a, b) {
return a + b
}
// 不纯函数示例
let counter = 0
function incrementCounter() {
counter++ // 修改了外部状态
return counter
}纯函数的三大特征
无状态性(Statelessness)
纯函数不依赖或修改函数外部的状态。它们只依赖于传入的参数,这使得函数的行为完全可预测。
javascript
// ❌ 不纯函数 - 依赖外部状态
let taxRate = 0.1
function calculateTax(price) {
return price * taxRate // 依赖外部变量 taxRate
}
// ✅ 纯函数 - 无状态性
function calculateTax(price, taxRate) {
return price * taxRate // 只依赖参数
}无副作用性(No Side Effects)
纯函数不会引起任何可观察的副作用,包括:
- 修改外部变量或对象
- 修改函数参数
- 执行 I/O 操作(如 console.log、网络请求)
- 操作 DOM
- 抛出异常(除非是函数参数本身的问题)
javascript
// ❌ 不纯函数 - 有副作用
function addItem(items, newItem) {
items.push(newItem) // 修改了原数组
return items
}
// ✅ 纯函数 - 无副作用
function addItem(items, newItem) {
return [...items, newItem] // 返回新数组,不修改原数组
}引用透明性(Referential Transparency)
引用透明性意味着函数调用可以被其返回值替代,而不会影响程序的行为。这个特性使得纯函数更容易推理和优化
javascript
// 纯函数具有引用透明性
function multiply(a, b) {
return a * b
}
// 在程序中,multiply(3, 4) 可以被直接替换为 12
const result1 = multiply(3, 4) + 10 // 等价于 const result1 = 12 + 10;
const result2 = 12 + 10 // 与上面的表达式等价典型应用
React 中的函数组件
React 的函数组件本质上是纯函数,给定相同的 props,总是渲染相同的 UI
javascript
// ✅ 纯函数组件
function Greeting({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
)
}
// ❌ 不纯的函数组件
let theme = "light"
function ThemedButton({ label }) {
// 依赖外部状态,不是纯函数
return <button className={theme}>{label}</button>
}Redux 中的 Reducer 函数
Redux 的 reducer 必须是纯函数,这是 Redux 架构的核心原则
javascript
// ✅ 纯函数reducer
function todoReducer(state = [], action) {
switch (action.type) {
case "ADD_TODO":
return [...state, action.payload] // 返回新数组
case "REMOVE_TODO":
return state.filter((todo) => todo.id !== action.payload)
default:
return state
}
}
// ❌ 不纯的reducer
function todoReducer(state = [], action) {
switch (action.type) {
case "ADD_TODO":
state.push(action.payload) // ❌ 直接修改原状态
return state
default:
return state
}
}Vue 的计算属性
Vue 的计算属性本质上是纯函数,它们基于依赖数据计算返回值
javascript
export default {
data() {
return {
firstName: "John",
lastName: "Doe"
}
},
computed: {
// ✅ 纯函数 - 计算属性
fullName() {
return `${this.firstName} ${this.lastName}`
},
// 纯函数使得计算属性可以缓存
reversedName() {
return this.fullName.split("").reverse().join("")
}
}
}纯函数与不纯函数的对比示例
数据处理对比
javascript
// 不纯的数据处理
class DataProcessor {
constructor() {
this.cache = {}
}
processData(data) {
// 修改外部状态(缓存)—— 副作用
this.cache[JSON.stringify(data)] = data.map((item) => item * 2)
return this.cache[JSON.stringify(data)]
}
}
// ✅ 正确的记忆化:缓存封装在内部,不污染外部
const DataProcessor = (() => {
const cache = new Map()
return {
// 纯函数 + 内部缓存
processData(data) {
const key = JSON.stringify(data)
if (cache.has(key)) {
return cache.get(key)
}
const result = data.map((item) => item * 2)
cache.set(key, result)
return result
}
}
})()状态管理对比
javascript
// 不纯的状态管理
let appState = {
user: null,
posts: []
}
function loginUser(username) {
appState.user = { name: username } // 直接修改全局状态
return appState
}
// 纯函数的状态管理
function loginUser(currentState, username) {
return {
...currentState,
user: { name: username }
}
}
// 使用纯函数的状态更新
let appState = {
user: null,
posts: []
}
appState = loginUser(appState, "John") // 返回新状态纯函数在前端性能优化中的作用
便于记忆化(Memoization)
纯函数的确定性使其非常适合记忆化优化
javascript
// 记忆化工具函数
function memoize(fn) {
const cache = new Map()
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log("从缓存获取结果")
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// 使用示例
const expensiveCalculation = memoize(function (n) {
console.log(`计算 ${n} 的斐波那契数`)
if (n <= 1) return n
return expensiveCalculation(n - 1) + expensiveCalculation(n - 2)
})
// 第一次计算
console.log(expensiveCalculation(40)) // 实际计算
console.log(expensiveCalculation(40)) // 从缓存获取更可靠的单元测试
纯函数使得单元测试变得简单直接,不需要复杂的设置和清理
javascript
// 纯函数的测试
import { calculateDiscount, formatPrice } from "./priceUtils"
describe("价格工具函数", () => {
test("calculateDiscount 应该正确计算折扣", () => {
expect(calculateDiscount(100, 0.2)).toBe(80)
expect(calculateDiscount(50, 0.5)).toBe(25)
expect(calculateDiscount(0, 0.1)).toBe(0)
})
test("formatPrice 应该正确格式化价格", () => {
expect(formatPrice(99.99)).toBe("¥99.99")
expect(formatPrice(1000)).toBe("¥1,000.00")
expect(formatPrice(0)).toBe("¥0.00")
})
test("不纯的格式化函数", () => {
// 测试变得复杂:需要模拟全局状态、重置副作用
setLocale("zh-CN")
expect(formatPriceImpure(99.99)).toBe("¥99.99")
setLocale("en-US") // 清理:需要还原状态
expect(formatPriceImpure(99.99)).toBe("$99.99")
})
})更好的可维护性
纯函数提高了代码的可读性和可维护性
javascript
// 纯函数的业务逻辑
const ShoppingCart = {
// 添加商品(纯函数,返回新数组)
addItem: (cart, item) => {
const existingItem = cart.find((i) => i.id === item.id)
if (existingItem) {
return cart.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
}
return [...cart, { ...item, quantity: 1 }]
},
// 计算总价(纯函数)
calculateTotal: (cart, taxRate) =>
cart.reduce((sum, i) => sum + i.price * i.quantity, 0) * (1 + taxRate),
// 应用折扣(纯函数)
applyDiscount: (total, code) =>
code === "SAVE10" ? total * 0.9 : total
let cart = []
cart = ShoppingCart.addItem(cart, { id: 1, name: "T-shirt", price: 29.99 })
cart = ShoppingCart.addItem(cart, { id: 2, name: "Jeans", price: 89.99 })
const total = ShoppingCart.calculateTotal(cart, 0.08)
const finalPrice = ShoppingCart.applyDiscount(total, "SAVE10")注意事项和最佳实践
识别和重构不纯函数
javascript
// ❌ 不纯函数 - 需要重构
class UserService {
constructor() {
this.users = []
}
addUser(user) {
this.users.push(user) // 修改内部状态
this.logActivity(user) // 副作用
return this.users
}
logActivity(user) {
console.log("用户活动:", user.name) // 副作用
}
}
// ✅ 重构:纯函数 + 副作用分离
// 纯函数 - 校验用户
function validateUser(user) {
return (
user.name &&
user.email &&
user.age > 0 &&
user.email.includes("@")
)
}
// 纯函数 - 添加用户(返回新数组)
function addUser(users, user) {
return [...users, user]
}
// 副作用 - 记录日志(独立函数)
function logActivity(user) {
console.log("用户活动:", user.name)
}
// 使用:副作用明确地在调用处执行
const users = []
const newUser = { name: "John", email: "john@example.com", age: 25 }
if (validateUser(newUser)) {
const updatedUsers = addUser(users, newUser) // 纯函数
logActivity(newUser) // 明确的副作用
}处理异步操作
javascript
// 纯函数处理异步数据
// 不纯的方式:在函数中直接发起网络请求
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`) // 副作用
return response.json()
}
// 更纯的方式:分离副作用
// 纯函数 - 数据处理
function processUserData(rawData) {
return {
id: rawData.id,
name: rawData.name,
email: rawData.email,
avatar: rawData.avatar_url
}
}
// 副作用分离
async function fetchAndProcessUser(userId) {
const response = await fetch(`/api/users/${userId}`)
const rawData = await response.json()
return processUserData(rawData) // 使用纯函数处理数据
}最佳实践清单
- 优先使用纯函数:对于数据转换、计算等操作,优先使用纯函数
- 明确分离副作用:将 I/O 操作、DOM 操作等副作用集中到专门的函数中
- 避免修改原数据:使用展开运算符、Object.assign 等创建新对象
- 使用类型系统:TypeScript 可以帮助识别潜在的副作用
- 编写测试:纯函数更容易测试,确保为重要的纯函数编写单元测试
- 利用框架优化:React 的 React.memo、Vue 的计算属性等都依赖纯函数特性
javascript
// 综合示例:遵循最佳实践的纯函数代码
import { memo } from "react"
// 纯函数工具库
const Utils = {
// 纯函数 - 数组操作
sortBy: (array, key) => {
return [...array].sort((a, b) => a[key] - b[key])
},
// 纯函数 - 对象操作
pick: (obj, keys) => {
return keys.reduce((result, key) => {
if (obj.hasOwnProperty(key)) {
result[key] = obj[key]
}
return result
}, {})
},
// 纯函数 - 按 id 查找
findById: (array, id) => array.find((item) => item.id === id) || null
}
// 使用 memo 优化的 React 组件
const UserList = memo(function UserList({ users, onSelect }) {
const sortedUsers = Utils.sortBy(users, "id")
return (
<ul>
{sortedUsers.map((user) => (
<li key={user.id} onClick={() => onSelect(Utils.pick(user, ["id", "name"]))}>
{user.name}
</li>
))}
</ul>
)
})
export default UserList总结
纯函数是前端开发中的重要概念,它提供了以下优势:
- 可预测性:相同输入总是得到相同输出
- 可测试性:无需复杂的测试设置
- 可缓存性:便于实现记忆化优化
- 可并行性:纯函数可以安全地并行执行
- 可维护性:代码逻辑清晰,易于理解和修改
在实际开发中,应该尽可能使用纯函数来处理核心业务逻辑,将副作用明确分离。这种编程方式不仅能提高代码质量,还能充分利用现代前端框架的优化特性,构建更加可靠和高效的应用程序