函数式编程

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.logconsole.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 边界允许副作用

最佳实践

  1. 核心逻辑纯函数:业务计算、数据转换用纯函数,副作用推到调用边界。
  2. 优先用数组方法filter/map/reduce 替代 for 循环。
  3. Immer 做不可变更新:比手动 spread 简洁且性能好。
  4. Pipe 优于 Compose:从左到右更符合阅读顺序。
  5. 渐进式采用:不必全盘函数式,先在数据处理层应用。

面试题

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 是最强大的——mapfilter 都可以用 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 操作、网络请求、定时器天然是副作用,强行纯化只会增加复杂度。


相关链接