Skip to content

JavaScript 进阶 十三:函数式编程

约 1932 字大约 6 分钟

javascript

2020-02-13

为什么 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)需要时间理解
  • 团队协作:确保团队成员都能理解函数式代码

总结

函数式编程不是银弹,但它提供了一种强大而优雅的解决问题的方式。通过拥抱纯函数、不可变数据和高阶函数,我们可以编写出更可预测、更易测试、更易维护的代码。

参考