JavaScript 进阶 十三:函数式编程
为什么 React 选择逐步告别 Class 组件,拥抱函数组件?为什么 Redux 的 Reducer 必须是纯函数?函数式思想正在以前所未有的速度渗透前端生态。本文将带你深入理解函数式编程的核心思想与实践方法。
什么是函数式编程?
函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算机运算视为数学函数的计算,并且避免使用程序状态以及易变对象。与命令式编程关注"如何做"不同,函数式编程更关注"做什么"。
核心特征
- 一等公民的函数:函数可以作为参数传递、可以作为返回值、可以赋值给变量
- 纯函数:相同的输入永远得到相同的输出,没有副作用
- 不可变性:数据一旦创建就不能被修改
- 声明式编程:描述要做什么,而不是如何做
纯函数:函数式编程的基石
纯函数的定义
纯函数示例
// 纯函数 - 相同的输入永远得到相同的输出
const add = (a, b) => a + b
const multiply = (x, y) => x * y
// 非纯函数 - 依赖外部状态,结果不确定
let counter = 0
function impureAdd(a, b) {
counter++ // 副作用
return a + b
}
// 非纯函数 - 修改了输入参数
function impureUpdateUser(user, newName) {
user.name = newName // 副作用
return user
}
纯函数的优势
- 可预测性:相同的输入永远得到相同的输出,便于测试和调试
- 可缓存性:可以轻松实现记忆化(memoization)
- 并行安全:没有共享状态,天然适合并发编程
- 组合性:纯函数可以轻松组合成更复杂的函数
高阶函数:函数的函数
高阶函数是指接收函数作为参数或返回函数作为结果的函数。
高阶函数实践
// 接收函数作为参数
const calculate = (operation, a, b) => operation(a, b)
const result = calculate((x, y) => x * y, 5, 3) // 15
// 返回函数作为结果
const createMultiplier = factor => number => number * factor
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
// 数组的高阶函数方法
const numbers = [1, 2, 3, 4, 5]
// map - 转换每个元素
const doubled = numbers.map(x => x * 2)
// filter - 过滤元素
const evens = numbers.filter(x => x % 2 === 0)
// reduce - 累积计算
const sum = numbers.reduce((acc, curr) => acc + curr, 0)
柯里化与函数组合
柯里化(Currying)
柯里化是将多参数函数转换为一系列单参数函数的技术。
柯里化实现与应用
// 手动柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
else {
return function (...args2) {
return curried.apply(this, args.concat(args2))
}
}
}
}
// 使用柯里化
const addThree = (a, b, c) => a + b + c
const curriedAdd = curry(addThree)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
// 实际应用:创建特定功能的函数
function createLogger(level) {
return message =>
console.log(`[${level.toUpperCase()}] ${message}`)
}
const infoLog = createLogger('info')
const errorLog = createLogger('error')
infoLog('Application started') // [INFO] Application started
errorLog('Something went wrong') // [ERROR] Something went wrong
函数组合(Compose)
函数组合是将多个函数组合成一个新函数的过程。
函数组合实践
// 简单的 compose 函数
function compose(...fns) {
return x =>
fns.reduceRight((acc, fn) => fn(acc), x)
}
// 实用的工具函数
const toUpperCase = str => str.toUpperCase()
const exclaim = str => `${str}!`
const repeat = str => `${str} ${str}`
// 组合使用
const shout = compose(exclaim, toUpperCase)
const dramaticShout = compose(repeat, exclaim, toUpperCase)
console.log(shout('hello')) // HELLO!
console.log(dramaticShout('hello')) // HELLO! HELLO!
// 数据处理管道
const users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 30, active: false },
{ name: 'Charlie', age: 35, active: true }
]
const processUsers = compose(
users => users.map(user => user.name),
users => users.filter(user => user.active),
users => users.filter(user => user.age > 25)
)
console.log(processUsers(users)) // ['Charlie']
不可变数据与持久化数据结构
不可变性的重要性
为什么需要不可变性?
在 JavaScript 中,对象和数组是引用类型,直接修改会导致意外的副作用和难以追踪的 bug。
不可变数据操作
// 错误的做法 - 直接修改
const user = { name: 'Alice', age: 25 }
user.age = 26 // 直接修改,可能产生副作用
// 正确的做法 - 创建新对象
const updatedUser = { ...user, age: 26 }
// 深层嵌套对象的不可变更新
const state = {
user: {
profile: {
name: 'Alice',
preferences: {
theme: 'dark',
language: 'en'
}
}
}
}
// 使用展开运算符进行深层更新
const newState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
preferences: {
...state.user.profile.preferences,
theme: 'light'
}
}
}
}
// 使用库简化不可变操作(如 Immer)
// import { produce } from 'immer';
// const newState = produce(state, draft => {
// draft.user.profile.preferences.theme = 'light';
// });
函子(Functor)与单子(Monad)
函子:带 map 方法的容器
函子实现
// 简单的 Box 函子
class Box {
constructor(value) {
this.value = value
}
map(fn) {
return new Box(fn(this.value))
}
fold(fn) {
return fn(this.value)
}
}
// 使用示例
const result = new Box(5)
.map(x => x * 2)
.map(x => x + 1)
.fold(x => x)
console.log(result) // 11
Maybe 单子:优雅处理空值
Maybe 单子
class Maybe {
constructor(value) {
this.value = value
}
static of(value) {
return new Maybe(value)
}
isNothing() {
return this.value === null || this.value === undefined
}
map(fn) {
return this.isNothing()
? Maybe.of(null)
: Maybe.of(fn(this.value))
}
chain(fn) {
return this.map(fn).fold()
}
fold() {
return this.value
}
}
// 使用示例
const getUser = id => Maybe.of(users.find(user => user.id === id))
const getEmail = user => Maybe.of(user.email)
const formatEmail = email => email.toUpperCase()
const email = getUser(1)
.chain(getEmail)
.chain(formatEmail)
.fold()
console.log(email) // 安全地处理可能的空值
实际应用场景
React 中的函数式编程
React 函数组件与 Hooks
import React, { useCallback, useMemo, useState } from 'react'
// 纯函数组件
function UserList({ users, onUserClick }) {
// 使用 useMemo 记忆化计算
const activeUsers = useMemo(
() => users.filter(user => user.active),
[users]
)
// 使用 useCallback 记忆化函数
const handleClick = useCallback(
(userId) => {
onUserClick(userId)
},
[onUserClick]
)
return (
<div>
{activeUsers.map(user => (
<div key={user.id} onClick={() => handleClick(user.id)}>
{user.name}
</div>
))}
</div>
)
}
// 自定义 Hook - 函数组合的体现
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
}
catch (error) {
return initialValue
}
})
const setValue = useCallback((value) => {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
}, [key])
return [storedValue, setValue]
}
Redux 中的纯函数
Redux Reducer
// 纯函数 reducer
const initialState = {
users: [],
loading: false,
error: null
}
function userReducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_USERS_START':
return {
...state,
loading: true,
error: null
}
case 'FETCH_USERS_SUCCESS':
return {
...state,
loading: false,
users: action.payload
}
case 'FETCH_USERS_FAILURE':
return {
...state,
loading: false,
error: action.payload
}
default:
return state
}
}
// Action creators - 纯函数
const fetchUsersStart = () => ({ type: 'FETCH_USERS_START' })
function fetchUsersSuccess(users) {
return {
type: 'FETCH_USERS_SUCCESS',
payload: users
}
}
性能优化技巧
记忆化(Memoization)
记忆化实现
// 简单的记忆化函数
function 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
}
}
// 使用示例
const expensiveCalculation = memoize((a, b) => {
console.log('Calculating...')
return a * b + Math.sqrt(a) + Math.sqrt(b)
})
console.log(expensiveCalculation(4, 9)) // Calculating... 然后输出结果
console.log(expensiveCalculation(4, 9)) // 直接从缓存输出结果
惰性求值
惰性求值模式
// 惰性序列
class LazySequence {
constructor(generator) {
this.generator = generator
this.cache = []
}
take(n) {
while (this.cache.length < n) {
const next = this.generator.next()
if (next.done)
break
this.cache.push(next.value)
}
return this.cache.slice(0, n)
}
map(fn) {
const originalGenerator = this.generator
return new LazySequence(function* () {
for (const value of originalGenerator) {
yield fn(value)
}
}())
}
}
最佳实践与常见陷阱
函数式编程最佳实践
- 优先使用纯函数:尽量减少副作用
- 拥抱不可变性:使用展开运算符或不可变库
- 善用高阶函数:map、filter、reduce 是你的好朋友
- 合理使用柯里化:但不要过度使用
- 保持函数简洁:单一职责原则
常见陷阱
- 过度抽象:不要为了函数式而函数式
- 性能问题:大量创建新对象可能影响性能
- 学习曲线:一些概念(如 Monad)需要时间理解
- 团队协作:确保团队成员都能理解函数式代码
总结
函数式编程不是银弹,但它提供了一种强大而优雅的解决问题的方式。通过拥抱纯函数、不可变数据和高阶函数,我们可以编写出更可预测、更易测试、更易维护的代码。
参考
- 《JavaScript 函数式编程指南》
- 《Eloquent JavaScript》函数式编程章节
- Ramda.js - 实用的函数式编程库
- Immer - 不可变状态管理