函数式编程
What — 什么是函数式编程
函数式编程(FP)是一种编程范式,将计算视为数学函数的求值,避免状态变化和可变数据。核心思想:用函数组合来构建程序,而非用命令式的步骤序列。
核心概念
| 概念 | 说明 |
|---|---|
| 纯函数 | 相同输入始终返回相同输出,无副作用 |
| 不可变性 | 数据创建后不可修改,修改则创建新副本 |
| 函数组合 | 小函数组合成大函数 |
| 高阶函数 | 接受函数作为参数或返回函数 |
| 柯里化 | 将多参数函数转为一系列单参数函数 |
| 声明式 | 描述”做什么”而非”怎么做” |
函数式 vs 命令式
// 命令式:怎么做
const result = []
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18) {
result.push(users[i].name)
}
}
// 函数式:做什么
const result = users
.filter(user => user.age >= 18)
.map(user => user.name)
Why — 为什么学函数式编程
1. 可预测性
纯函数没有副作用,调用 100 次结果相同。调试时只需关注输入输出,无需追踪状态变化。
2. 可组合性
小函数像乐高积木,可以自由组合。compose(filterAdults, mapNames, sortByAge)(users) 一行完成复杂逻辑。
3. 可测试性
纯函数测试只需断言输入输出,不需要 mock 数据库、API、全局状态。
4. 并发安全
不可变数据不存在竞态条件,天生适合并行处理。
How — 核心技术详解
1. 纯函数
// ✅ 纯函数:相同输入,相同输出,无副作用
function add(a, b) {
return a + b
}
function formatPrice(price) {
return `¥${price.toFixed(2)}`
}
// ❌ 非纯函数:依赖外部状态
let discount = 0.9
function calculatePrice(price) {
return price * discount // 依赖外部变量 discount
}
// ✅ 改为纯函数
function calculatePrice(price, discount) {
return price * discount
}
副作用的类型:
| 副作用 | 示例 |
|---|---|
| 修改外部变量 | let count = 0; function inc() { count++ } |
| DOM 操作 | document.getElementById('x').textContent = 'hi' |
| API 调用 | fetch('/api/data') |
| console.log | console.log('debug') |
| 修改参数 | function push(arr, item) { arr.push(item) } |
实际项目中的策略:核心逻辑用纯函数,副作用推到边界(IO 层)。
2. 不可变数据
// ❌ 可变操作
const user = { name: 'Alice', age: 25 }
user.age = 26 // 直接修改
const items = [1, 2, 3]
items.push(4) // 直接修改
// ✅ 不可变操作
const user = { name: 'Alice', age: 25 }
const updatedUser = { ...user, age: 26 } // 创建新对象
const items = [1, 2, 3]
const newItems = [...items, 4] // 创建新数组
// 嵌套更新
const state = {
user: { name: 'Alice', address: { city: 'Shanghai' } }
}
const newState = {
...state,
user: {
...state.user,
address: { ...state.user.address, city: 'Beijing' }
}
}
Immer 简化不可变更新:
import { produce } from 'immer'
const newState = produce(state, draft => {
draft.user.address.city = 'Beijing' // 看似可变,实际产生新对象
})
3. 高阶函数
// 接受函数作为参数
function map(arr, fn) {
const result = []
for (const item of arr) {
result.push(fn(item))
}
return result
}
// 返回函数
function multiply(a) {
return function(b) {
return a * b
}
}
const double = multiply(2)
double(5) // 10
double(10) // 20
常用高阶函数:
const users = [
{ name: 'Alice', age: 25, role: 'admin' },
{ name: 'Bob', age: 17, role: 'user' },
{ name: 'Charlie', age: 30, role: 'user' },
]
// filter:过滤
const adults = users.filter(u => u.age >= 18)
// map:映射
const names = users.map(u => u.name)
// reduce:归约
const totalAge = users.reduce((sum, u) => sum + u.age, 0)
// find:查找
const admin = users.find(u => u.role === 'admin')
// every / some
const allAdults = users.every(u => u.age >= 18) // false
const hasAdmin = users.some(u => u.role === 'admin') // true
// 链式组合
const adultNames = users
.filter(u => u.age >= 18)
.map(u => u.name.toUpperCase())
.sort()
4. 柯里化(Currying)
// 柯里化:f(a, b, c) → f(a)(b)(c)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs])
}
}
}
// 使用
const add = curry((a, b, c) => a + b + c)
add(1)(2)(3) // 6
add(1, 2)(3) // 6
add(1)(2, 3) // 6
// 实战:创建可复用的过滤函数
const filter = curry((predicate, arr) => arr.filter(predicate))
const map = curry((fn, arr) => arr.map(fn))
const filterAdults = filter(u => u.age >= 18)
const mapNames = map(u => u.name)
const adultNames = compose(mapNames, filterAdults)(users)
偏函数应用(Partial Application):
// 预设部分参数
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs)
}
}
const multiply = (a, b, c) => a * b * c
const double = partial(multiply, 2) // 预设 a = 2
double(3, 4) // 24
// 实战
const fetchWithAuth = partial(fetch, undefined, {
headers: { Authorization: `Bearer ${getToken()}` }
})
5. 函数组合(Compose / Pipe)
// compose:从右到左执行
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x)
// pipe:从左到右执行(更易读)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)
// 使用
const trim = s => s.trim()
const toLower = s => s.toLowerCase()
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
const normalize = pipe(trim, toLower, capitalize)
normalize(' HELLO WORLD ') // 'Hello world'
// 数据处理管道
const getAdultNames = pipe(
filter(u => u.age >= 18),
map(u => u.name),
names => names.sort()
)
getAdultNames(users) // ['Alice', 'Charlie']
6. 函子(Functor)与 Monad
// Maybe 函子:处理可能为 null 的值
class Maybe {
constructor(value) {
this.value = value
}
static of(value) {
return new Maybe(value)
}
map(fn) {
return this.value == null
? Maybe.of(null)
: Maybe.of(fn(this.value))
}
getOrElse(defaultValue) {
return this.value == null ? defaultValue : this.value
}
}
// 使用
const user = { address: { street: 'Main St' } }
// 传统方式
const street = user && user.address && user.address.street
// Maybe
const street2 = Maybe.of(user)
.map(u => u.address)
.map(a => a.street)
.getOrElse('Unknown')
// Either:处理错误
class Either {
static Left(value) { return { isLeft: true, value } }
static Right(value) { return { isLeft: false, value } }
static try(fn) {
try {
return Either.Right(fn())
} catch (e) {
return Either.Left(e)
}
}
}
const result = Either.try(() => JSON.parse(input))
result.isLeft // true if error
result.value // parsed data or error
7. 实战:函数式的状态管理
// 函数式 Reducer 模式(Redux 核心)
type State = { users: User[]; loading: boolean; error: string | null }
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_ERROR'; payload: string }
// 纯函数 Reducer
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, users: action.payload }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload }
default:
return state
}
}
// 使用
const state1 = { users: [], loading: false, error: null }
const state2 = reducer(state1, { type: 'FETCH_START' })
const state3 = reducer(state2, { type: 'FETCH_SUCCESS', payload: [{ name: 'Alice' }] })
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 性能差(大量创建新对象) | 不可变数据每次都创建新副本 | 用 Immer 优化结构共享 |
| 柯里化过度 | 简单函数也柯里化,可读性下降 | 只在需要参数复用时柯里化 |
| Monad 难懂 | 函数式概念抽象 | 从 Maybe/Either 入手,逐步理解 |
| 过度函数式 | 所有代码都追求纯函数 | 核心逻辑纯函数,IO 边界允许副作用 |
最佳实践
- 核心逻辑纯函数:业务计算、数据转换用纯函数,副作用推到调用边界。
- 优先用数组方法:
filter/map/reduce替代 for 循环。 - Immer 做不可变更新:比手动 spread 简洁且性能好。
- Pipe 优于 Compose:从左到右更符合阅读顺序。
- 渐进式采用:不必全盘函数式,先在数据处理层应用。
面试题
1. 什么是纯函数?为什么它在函数式编程中重要?
答:纯函数满足两个条件:(1) 相同输入始终返回相同输出;(2) 没有副作用(不修改外部状态、不做 IO)。它重要的原因:(1) 可预测——调用 1000 次结果相同,行为完全确定;(2) 可测试——只需断言输入输出,无需 mock 任何外部依赖;(3) 可缓存——相同输入可以缓存结果(memoization);(4) 可并行——无共享状态,不存在竞态条件;(5) 可组合——纯函数可以自由组合,无需担心执行顺序影响结果。
2. 柯里化和偏函数应用有什么区别?
答:柯里化是将多参数函数 f(a, b, c) 转换为一系列单参数函数 f(a)(b)(c)——每次只接受一个参数,返回新函数等待下一个参数。偏函数应用是预设函数的部分参数 f(a, _, c) → g(b),剩余参数一次性传入。区别:(1) 柯里化产生嵌套的一元函数链,偏函数产生参数更少的函数;(2) 柯里化总是从左到右逐个参数,偏函数可以预设任意位置的参数。实际中偏函数应用更常用(如 partial(fetch, url, { headers })),柯里化更多用于函数式组合。
3. JavaScript 中的 map/filter/reduce 是如何体现函数式思想的?
答:三个核心体现:(1) 声明式——users.filter(isAdult).map(getName) 描述”做什么”(过滤成年人、取名字),而非”怎么做”(for 循环 + if + push);(2) 纯函数——回调函数不修改原数组,返回新数组,保证不可变性;(3) 组合性——三个方法可以链式组合,每个步骤是一个纯函数变换,整体构成数据管道。reduce 是最强大的——map 和 filter 都可以用 reduce 实现,它是函数式编程的”万能原语”。
4. 什么是 Monad?用通俗的方式解释。
答:Monad 是一种设计模式,用于链式处理”带上下文的值”。通俗理解:Monad 是一个”盒子”,盒子里面装着值,你可以对盒子里的值做操作(map),操作结果还是同类型的盒子。常见的 Monad:(1) Maybe——盒子可能为空,map 时自动跳过空值,避免 null.xxx 报错;(2) Either——盒子可能装了错误,map 时自动跳过错误分支;(3) Promise——盒子可能还没就绪,.then 等就绪后自动执行。Monad 的核心价值是把”错误处理""空值判断""异步等待”等横切关注点封装在盒子内部,让业务代码只关注正常逻辑。
5. 为什么 Redux 的 Reducer 必须是纯函数?
答:三个原因:(1) 时间旅行调试——Redux DevTools 可以回放每个 action 的状态变化,前提是相同 state + action 必须产生相同的新 state。如果 reducer 有副作用(如随机数、API 调用),回放时结果不同,时间旅行失效;(2) 状态可预测——纯函数保证 state 变化完全由 action 决定,不存在隐藏的状态变化来源;(3) 中间件机制——Redux 的中间件(如 redux-thunk、redux-saga)在 reducer 之前拦截副作用,reducer 只做纯计算,职责分离清晰。
6. Immer 是如何实现”看似可变、实际不可变”的?
答:Immer 使用 ES6 Proxy 拦截对象的所有写操作。当你写 draft.user.age = 26 时:(1) Proxy 拦截赋值操作;(2) Immer 在内部创建该层级的浅拷贝(结构共享——未修改的子对象仍引用原对象);(3) 修改写入浅拷贝而非原始对象;(4) produce 函数返回新的根对象,其中只有被修改路径上的对象是新创建的,其余对象与原 state 共享引用。这就是”结构共享”——既保证不可变性,又避免深拷贝的性能开销。
7. 函数组合(compose/pipe)和中间件模式有什么关系?
答:中间件模式是函数组合的特例——带”包裹”的组合。普通 compose 是 f(g(h(x))),数据从右向左流经每个函数。中间件是 middleware1(middleware2(middleware3(handler)))(action),每个中间件可以在调用下一个函数前后执行逻辑(如日志、错误处理、权限检查)。Koa 的洋葱模型和 Redux 的中间件都是这种模式。本质上中间件 = compose + 在每个函数中调用 next() 将控制权传递给下一个函数。
8. 前端项目中如何平衡函数式和命令式?
答:核心原则——核心逻辑纯函数,副作用推到边界。具体策略:(1) 数据层——数据转换、过滤、排序用纯函数+管道组合;(2) 状态管理——Reducer 用纯函数(Redux/Zustand),副作用在 middleware 或 thunk 中;(3) UI 层——组件本身是命令式的(操作 DOM),但事件处理调用纯函数计算新状态;(4) API 层——API 调用是副作用,但请求参数的构建和响应数据的转换用纯函数。不要追求 100% 纯函数——DOM 操作、网络请求、定时器天然是副作用,强行纯化只会增加复杂度。