Skip to content

函数式编程

纯函数

定义

纯函数(Pure Function)满足两个条件:

  1. 确定性:相同的输入永远产生相同的输出
  2. 无副作用:不修改外部状态,不依赖外部可变状态
js
function add(a, b) {
  return a + b
}

add(1, 2) === add(1, 2)

以下是非纯函数的典型例子:

js
let count = 0
function increment() {
  return ++count
}

function getRandomId() {
  return Math.random().toString(36).slice(2)
}

function formatDate() {
  return new Date().toISOString()
}

function saveToStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value))
}

副作用

副作用(Side Effect)是指函数执行过程中对外部环境产生的可观察变化,包括但不限于:

  • 修改全局变量或外部对象属性
  • DOM 操作
  • 网络请求(HTTP/WebSocket)
  • 读写文件、localStorage、Cookie
  • 输出到控制台(console.log
  • 修改函数参数(引用类型)
  • 抛出异常
  • 调用其他含副作用的函数

函数式编程并非消灭副作用——没有副作用的程序什么都做不了。核心思想是将副作用隔离、推迟、集中管理,使业务逻辑的核心部分保持纯粹。

引用透明性

引用透明性(Referential Transparency)是纯函数的推论:一个表达式可以被它的计算结果替换,而不改变程序的行为。

js
function double(x) {
  return x * 2
}

const result = double(5) + double(3)
const result2 = 10 + 6

resultresult2 完全等价。这意味着:

  • 编译器可以安全地进行常量折叠公共子表达式消除优化
  • 代码可以自由地并行执行(无共享可变状态)
  • 测试变得极其简单——只需要断言输入输出关系,无需 mock 外部环境
  • 可以安全地缓存/记忆化函数结果

不可变数据

Object.freeze

Object.freeze 是 JavaScript 原生的浅冻结方法,被冻结的对象不能添加、删除或修改属性:

js
const config = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json'
  }
})

config.apiUrl = 'https://other.com'
config.newProp = 'value'
delete config.timeout
config.headers.Authorization = 'Bearer token'

Object.freeze浅冻结——嵌套对象仍可修改。实现深冻结需要递归处理:

js
function deepFreeze(obj) {
  Object.freeze(obj)

  Object.getOwnPropertyNames(obj).forEach(prop => {
    const value = obj[prop]
    if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
      deepFreeze(value)
    }
  })

  return obj
}

Immer 原理

Immer 通过 Copy-on-Write(写时复制) 机制实现不可变更新。核心原理是使用 Proxy 拦截对草稿对象的写操作,只在实际修改时才创建浅拷贝:

js
function produce(base, recipe) {
  const copies = new Map()
  const proxies = new Map()

  function createProxy(target) {
    if (proxies.has(target)) return proxies.get(target)

    const handler = {
      get(_, prop) {
        const source = copies.get(target) || target
        const value = source[prop]
        if (value !== null && typeof value === 'object') {
          return createProxy(value)
        }
        return value
      },

      set(_, prop, value) {
        if (!copies.has(target)) {
          copies.set(target, Array.isArray(target) ? [...target] : { ...target })
        }
        copies.get(target)[prop] = value
        return true
      },

      deleteProperty(_, prop) {
        if (!copies.has(target)) {
          copies.set(target, Array.isArray(target) ? [...target] : { ...target })
        }
        delete copies.get(target)[prop]
        return true
      }
    }

    const proxy = new Proxy(target, handler)
    proxies.set(target, proxy)
    return proxy
  }

  const draft = createProxy(base)
  recipe(draft)

  function finalize(target) {
    if (copies.has(target)) {
      const copy = copies.get(target)
      Object.keys(copy).forEach(key => {
        if (typeof copy[key] === 'object' && copy[key] !== null) {
          copy[key] = finalize(copy[key])
        }
      })
      return copy
    }
    return target
  }

  return finalize(base)
}

const state = {
  users: [
    { id: 1, name: 'Alice', scores: [90, 85] },
    { id: 2, name: 'Bob', scores: [78, 92] }
  ],
  settings: { theme: 'dark' }
}

const nextState = produce(state, draft => {
  draft.users[0].name = 'Alice Updated'
  draft.users[0].scores.push(95)
})

state.users[0].name
nextState.users[0].name
state.settings === nextState.settings
state.users[1] === nextState.users[1]

结构化共享

结构化共享(Structural Sharing)是不可变数据结构的核心优化策略:更新操作只创建被修改路径上的新节点,未修改的子树共享引用。

原始状态                    新状态
    root                     root'
   /    \                   /    \
  A      B        →       A'     B  ← 共享引用
 / \    / \              / \    / \
a1  a2 b1  b2           a1' a2 b1  b2

                      仅修改此节点

这意味着:

  • 空间复杂度:修改一个深度为 d 的节点,只需创建 d 个新节点
  • 比较优化:可以通过引用相等(===)快速判断子树是否变化,这是 React 性能优化(React.memoshouldComponentUpdate)的基础
  • Immutable.js 的 Trie 数据结构和 Immer 的 Proxy 方案都基于此原理

高阶函数

高阶函数(Higher-Order Function)是至少满足以下条件之一的函数:

  • 接受函数作为参数
  • 返回一个函数

map 手写实现

js
Array.prototype.myMap = function (fn, thisArg) {
  if (typeof fn !== 'function') {
    throw new TypeError(`${fn} is not a function`)
  }

  const result = new Array(this.length)

  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      result[i] = fn.call(thisArg, this[i], i, this)
    }
  }

  return result
}

const doubled = [1, 2, 3].myMap(x => x * 2)

const sparse = [1, , 3]
sparse.myMap(x => x * 2)

i in this 的判断是为了正确处理稀疏数组——跳过空位(hole),保持与原生 map 行为一致。

filter 手写实现

js
Array.prototype.myFilter = function (fn, thisArg) {
  if (typeof fn !== 'function') {
    throw new TypeError(`${fn} is not a function`)
  }

  const result = []

  for (let i = 0; i < this.length; i++) {
    if (i in this && fn.call(thisArg, this[i], i, this)) {
      result.push(this[i])
    }
  }

  return result
}

const evens = [1, 2, 3, 4, 5].myFilter(x => x % 2 === 0)

reduce 手写实现

js
Array.prototype.myReduce = function (fn, initialValue) {
  if (typeof fn !== 'function') {
    throw new TypeError(`${fn} is not a function`)
  }

  const len = this.length
  let accumulator
  let startIndex = 0

  if (arguments.length >= 2) {
    accumulator = initialValue
  } else {
    let found = false
    while (startIndex < len) {
      if (startIndex in this) {
        accumulator = this[startIndex]
        startIndex++
        found = true
        break
      }
      startIndex++
    }
    if (!found) {
      throw new TypeError('Reduce of empty array with no initial value')
    }
  }

  for (let i = startIndex; i < len; i++) {
    if (i in this) {
      accumulator = fn(accumulator, this[i], i, this)
    }
  }

  return accumulator
}

const sum = [1, 2, 3, 4].myReduce((acc, cur) => acc + cur, 0)

const grouped = ['one', 'two', 'three'].myReduce((acc, word) => {
  const key = word.length
  acc[key] = acc[key] || []
  acc[key].push(word)
  return acc
}, {})

reduce 是最强大的数组方法——mapfilterflatMapeverysome 等方法都可以用 reduce 实现:

js
function mapWithReduce(arr, fn) {
  return arr.reduce((acc, item, index) => {
    acc.push(fn(item, index, arr))
    return acc
  }, [])
}

function filterWithReduce(arr, fn) {
  return arr.reduce((acc, item, index) => {
    if (fn(item, index, arr)) acc.push(item)
    return acc
  }, [])
}

function flatMapWithReduce(arr, fn) {
  return arr.reduce((acc, item, index) => {
    return acc.concat(fn(item, index, arr))
  }, [])
}

柯里化

概念

柯里化(Currying)是将一个接受多个参数的函数转换为一系列接受单个参数的函数的过程。

js
function add(a, b, c) {
  return a + b + c
}

function curriedAdd(a) {
  return function (b) {
    return function (c) {
      return a + b + c
    }
  }
}

curriedAdd(1)(2)(3)

手写 curry 实现

js
function curry(fn) {
  const arity = fn.length

  return function curried(...args) {
    if (args.length >= arity) {
      return fn.apply(this, args)
    }

    return function (...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs])
    }
  }
}

const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)

curriedAdd(1)(2)(3)
curriedAdd(1, 2)(3)
curriedAdd(1)(2, 3)
curriedAdd(1, 2, 3)

偏应用(Partial Application)

偏应用是固定函数的部分参数,返回一个接受剩余参数的新函数。与柯里化不同,偏应用不要求每次只传一个参数:

js
function partial(fn, ...presetArgs) {
  return function (...laterArgs) {
    return fn.apply(this, [...presetArgs, ...laterArgs])
  }
}

function createLogger(level, prefix, message) {
  console.log(`[${level}] ${prefix}: ${message}`)
}

const errorLog = partial(createLogger, 'ERROR')
const authError = partial(createLogger, 'ERROR', 'Auth')

errorLog('Network', 'Connection failed')
authError('Token expired')

占位符

支持占位符的柯里化允许跳过某些参数位置,后续再填充:

js
const _ = Symbol('placeholder')

function curryWithPlaceholder(fn) {
  const arity = fn.length

  return function curried(...args) {
    const realArgs = args.slice(0, arity)
    const hasPlaceholder = realArgs.some(arg => arg === _)

    if (realArgs.length >= arity && !hasPlaceholder) {
      return fn.apply(this, realArgs)
    }

    return function (...moreArgs) {
      const merged = []
      let moreIndex = 0

      for (let i = 0; i < realArgs.length; i++) {
        if (realArgs[i] === _ && moreIndex < moreArgs.length) {
          merged.push(moreArgs[moreIndex++])
        } else {
          merged.push(realArgs[i])
        }
      }

      while (moreIndex < moreArgs.length) {
        merged.push(moreArgs[moreIndex++])
      }

      return curried.apply(this, merged)
    }
  }
}

const match = curryWithPlaceholder((regex, str) => str.match(regex))
const replace = curryWithPlaceholder((regex, replacement, str) => str.replace(regex, replacement))

const matchDigits = match(/\d+/g)
matchDigits('hello123world456')

const censor = replace(/badword/gi, '***')
censor('This is a badword example')

const replaceInHello = replace(_, _, 'hello world')
replaceInHello(/world/, 'JavaScript')

函数组合

compose 手写实现

compose 从右到左组合多个函数,前一个函数的输出作为后一个函数的输入:

js
function compose(...fns) {
  if (fns.length === 0) return (arg) => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce((a, b) => {
    return (...args) => a(b(...args))
  })
}

const toUpper = str => str.toUpperCase()
const exclaim = str => `${str}!`
const repeat = str => `${str} ${str}`

const shout = compose(repeat, exclaim, toUpper)
shout('hello')

执行顺序:toUpper('hello')'HELLO'exclaim('HELLO')'HELLO!'repeat('HELLO!')'HELLO! HELLO!'

pipe 手写实现

pipecompose 相反,从左到右执行,更符合阅读习惯:

js
function pipe(...fns) {
  if (fns.length === 0) return (arg) => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce((a, b) => {
    return (...args) => b(a(...args))
  })
}

const processUser = pipe(
  (user) => ({ ...user, name: user.name.trim() }),
  (user) => ({ ...user, name: user.name.toLowerCase() }),
  (user) => ({ ...user, email: `${user.name}@example.com` }),
  (user) => ({ ...user, createdAt: Date.now() })
)

processUser({ name: '  Alice  ' })

异步 pipe

js
function pipeAsync(...fns) {
  return function (input) {
    return fns.reduce(
      (chain, fn) => chain.then(fn),
      Promise.resolve(input)
    )
  }
}

const processOrder = pipeAsync(
  async (order) => {
    const validated = await validateOrder(order)
    return { ...order, validated }
  },
  async (order) => {
    const total = await calculateTotal(order)
    return { ...order, total }
  },
  async (order) => {
    const result = await submitOrder(order)
    return { ...order, ...result }
  }
)

Point-free 风格

Point-free(无参数)风格是指在定义函数时不显式指明参数,而是通过函数组合来描述数据转换过程:

js
const split = (sep) => (str) => str.split(sep)
const join = (sep) => (arr) => arr.join(sep)
const map = (fn) => (arr) => arr.map(fn)
const filter = (fn) => (arr) => arr.filter(fn)
const trim = (str) => str.trim()
const toLower = (str) => str.toLowerCase()
const length = (arr) => arr.length

const countWords = pipe(
  trim,
  split(/\s+/),
  filter(Boolean),
  length
)

countWords('  hello   world  foo  ')

const slugify = pipe(
  toLower,
  trim,
  split(/\s+/),
  join('-')
)

slugify('  Hello World Example  ')

const getShortNames = pipe(
  filter((name) => name.length <= 4),
  map(toUpper)
)

getShortNames(['Alice', 'Bob', 'Eve', 'Charlie', 'Dan'])

Point-free 风格的优势是声明式、高度可读,但过度使用会降低可读性——需要在简洁与清晰之间取得平衡。


Functor 与 Monad

Functor(函子)

Functor 是一个实现了 map 方法的容器类型,它将一个函数应用到容器内部的值上,返回一个新的同类型容器。Functor 必须满足两条定律:

  • 同一律functor.map(x => x) 等价于 functor
  • 组合律functor.map(f).map(g) 等价于 functor.map(x => g(f(x)))
js
class Container {
  constructor(value) {
    this._value = value
  }

  static of(value) {
    return new Container(value)
  }

  map(fn) {
    return Container.of(fn(this._value))
  }

  inspect() {
    return `Container(${JSON.stringify(this._value)})`
  }
}

Container.of(3)
  .map(x => x + 1)
  .map(x => x * 2)
  .inspect()

数组就是一个 Functor——[].map(fn) 返回新数组,满足同一律和组合律。Promise 也近似 Functor(then 方法)。

Maybe Functor

Maybe 用于处理可能为空的值,安全地进行链式操作而无需手动判空:

js
class Maybe {
  constructor(value) {
    this._value = value
  }

  static of(value) {
    return new Maybe(value)
  }

  get isNothing() {
    return this._value === null || this._value === undefined
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this._value))
  }

  getOrElse(defaultValue) {
    return this.isNothing ? defaultValue : this._value
  }

  filter(fn) {
    if (this.isNothing) return this
    return fn(this._value) ? this : Maybe.of(null)
  }

  inspect() {
    return this.isNothing ? 'Maybe(Nothing)' : `Maybe(${JSON.stringify(this._value)})`
  }
}

function safeGet(obj, path) {
  return path.split('.').reduce(
    (maybe, key) => maybe.map(o => o[key]),
    Maybe.of(obj)
  )
}

const user = {
  name: 'Alice',
  address: {
    city: 'Beijing',
    zip: '100000'
  }
}

safeGet(user, 'address.city').getOrElse('Unknown')
safeGet(user, 'address.street').getOrElse('Unknown')
safeGet(null, 'address.city').getOrElse('Unknown')

与可选链(?.)对比:Maybe 的优势在于它是一个可组合的容器——可以使用 mapfilterflatMap 等方法进行链式函数组合,而 ?. 只是语法糖,不具备组合能力。

Either Functor

Either 用于处理可能失败的操作,提供 Left(错误路径)和 Right(成功路径)两种状态:

js
class Left {
  constructor(value) {
    this._value = value
  }

  map() {
    return this
  }

  flatMap() {
    return this
  }

  getOrElse(defaultValue) {
    return defaultValue
  }

  fold(leftFn, _rightFn) {
    return leftFn(this._value)
  }

  inspect() {
    return `Left(${JSON.stringify(this._value)})`
  }
}

class Right {
  constructor(value) {
    this._value = value
  }

  map(fn) {
    return new Right(fn(this._value))
  }

  flatMap(fn) {
    return fn(this._value)
  }

  getOrElse() {
    return this._value
  }

  fold(_leftFn, rightFn) {
    return rightFn(this._value)
  }

  inspect() {
    return `Right(${JSON.stringify(this._value)})`
  }
}

function tryCatch(fn) {
  try {
    return new Right(fn())
  } catch (e) {
    return new Left(e.message)
  }
}

const parseJSON = (str) => tryCatch(() => JSON.parse(str))

parseJSON('{"name": "Alice"}')
  .map(data => data.name)
  .map(name => name.toUpperCase())
  .fold(
    error => `Error: ${error}`,
    value => `Success: ${value}`
  )

parseJSON('invalid json')
  .map(data => data.name)
  .fold(
    error => `Error: ${error}`,
    value => `Success: ${value}`
  )

Monad

Monad 是实现了 flatMap(也叫 chain/bind)方法的 Functor。flatMap 解决了 Functor 嵌套的问题——当 map 的回调返回的也是一个 Functor 时,flatMap 会将结果"拍平"一层:

js
class IO {
  constructor(effect) {
    this._effect = effect
  }

  static of(value) {
    return new IO(() => value)
  }

  map(fn) {
    return new IO(() => fn(this._effect()))
  }

  flatMap(fn) {
    return new IO(() => fn(this._effect())._effect())
  }

  run() {
    return this._effect()
  }
}

const readInput = new IO(() => document.querySelector('#input').value)
const toUpper = (str) => IO.of(str.toUpperCase())
const writeOutput = (str) => new IO(() => {
  document.querySelector('#output').textContent = str
  return str
})

const program = readInput
  .flatMap(toUpper)
  .flatMap(writeOutput)

Monad 定律:

  • 左单位律Monad.of(a).flatMap(f) 等价于 f(a)
  • 右单位律m.flatMap(Monad.of) 等价于 m
  • 结合律m.flatMap(f).flatMap(g) 等价于 m.flatMap(x => f(x).flatMap(g))

Promise 是 JavaScript 中最常见的"近似 Monad"——then 方法同时承担了 mapflatMap 的职责(自动拍平返回的 Promise)。


前端中的函数式实践

React Hooks

React Hooks 深度贯彻函数式编程思想:

js
function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState)

  const dispatch = useCallback((action) => {
    setState(prevState => reducer(prevState, action))
  }, [reducer])

  return [state, dispatch]
}

reducer 是纯函数——相同的 (state, action) 必定产生相同的 newStateuseState 本质上是一个 State Monad 的简化版本。

自定义 Hook 即函数组合

js
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

function useSearch(query) {
  const debouncedQuery = useDebounce(query, 300)
  const [results, setResults] = useState([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    if (!debouncedQuery) {
      setResults([])
      return
    }

    setLoading(true)
    searchAPI(debouncedQuery)
      .then(setResults)
      .finally(() => setLoading(false))
  }, [debouncedQuery])

  return { results, loading }
}

自定义 Hook 是数据管道的组合——useSearch 组合了 useDebounceuseStateuseEffect,形成从输入到输出的函数式数据流。

Redux Reducer

Redux 的核心模式完全是函数式的:

js
const initialState = {
  items: [],
  loading: false,
  error: null
}

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }

    case 'FETCH_SUCCESS':
      return { ...state, loading: false, items: action.payload }

    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload }

    case 'ADD_TODO':
      return { ...state, items: [...state.items, action.payload] }

    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        )
      }

    case 'REMOVE_TODO':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      }

    default:
      return state
  }
}

函数式特征解析:

  • reducer 是纯函数:(state, action) → newState
  • 状态不可变:通过展开运算符创建新对象,不修改原 state
  • 引用透明:相同的 state + action 必然产生相同的 newState
  • combineReducers 本质是函数组合

RxJS

RxJS 将异步数据流建模为 Observable(可观察序列),通过操作符进行函数式转换:

js
import { fromEvent, interval } from 'rxjs'
import {
  map,
  filter,
  debounceTime,
  switchMap,
  takeUntil,
  scan,
  distinctUntilChanged
} from 'rxjs/operators'

const search$ = fromEvent(document.querySelector('#search'), 'input').pipe(
  map(event => event.target.value),
  debounceTime(300),
  distinctUntilChanged(),
  filter(query => query.length >= 2),
  switchMap(query =>
    fetch(`/api/search?q=${encodeURIComponent(query)}`).then(r => r.json())
  )
)

search$.subscribe({
  next: results => renderResults(results),
  error: err => showError(err)
})

const counter$ = interval(1000).pipe(
  scan((count) => count + 1, 0),
  takeUntil(fromEvent(document.querySelector('#stop'), 'click'))
)

RxJS 中的函数式概念映射:

  • Observable 是 Monad(pipe + 操作符 = flatMap 链)
  • 操作符是纯函数:输入 Observable,输出新 Observable
  • pipe 就是函数组合
  • scan 类似 reduce(但对流数据持续累积)
  • mapfilter 与数组方法同构

用心学习,用代码说话 💻