函数式编程基础

张玥 2026年2月14日 阅读时间 45分钟
JavaScript 函数式编程 纯函数 柯里化 组合
函数式编程概念图

函数式编程(Functional Programming,FP)是一种编程范式,它将计算视为数学函数的求值,并避免使用共享状态、可变数据以及副作用。JavaScript 作为多范式语言,对函数式编程有着天然的支持。本文将系统讲解函数式编程的核心思想、基础概念以及在实际开发中的应用,通过大量代码示例帮助您掌握纯函数、高阶函数、柯里化、组合、函子等关键知识。无论你是初学者还是希望提升代码质量的开发者,都能从中获得启发。

1. 函数式编程概述

函数式编程起源于λ演算,强调使用表达式而非语句,通过组合函数来构建程序逻辑。它与命令式编程的主要区别在于:函数式代码描述“做什么”,而不是“如何做”。

// 命令式(告诉如何做)
let arr = [1, 2, 3, 4];
let doubled = [];
for (let i = 0; i < arr.length; i++) {
  doubled.push(arr[i] * 2);
}

// 函数式(声明式,描述做什么)
const arr = [1, 2, 3, 4];
const doubled = arr.map(n => n * 2);
🔹 核心特征
  • 不可变性 (Immutability)
  • 纯函数 (Pure functions)
  • 函数组合 (Composition)
  • 引用透明 (Referential transparency)
  • 惰性求值 (Lazy evaluation)

2. 纯函数(Pure Functions)

纯函数是指:相同的输入永远得到相同的输出,并且没有任何可观察的副作用(如修改外部变量、I/O操作)。

// 纯函数
const add = (a, b) => a + b;
const toUpper = str => str.toUpperCase();

// 不纯的函数(依赖外部变量)
let tax = 0.1;
const calculatePrice = price => price * (1 + tax);

// 不纯的函数(修改外部状态)
let count = 0;
const increment = () => count++;

纯函数优点:

  • 可缓存(Memoization)
  • 可移植/自文档化
  • 可并行执行(无竞争)
  • 易于测试

3. 不可变性(Immutability)

数据一旦创建就不能被修改。要改变数据,必须创建新的副本。JavaScript 中可用 const 以及对象展开、Object.freeze 等方式实现。

// 可变(mutate)
const obj = { name: '张三' };
obj.name = '李四';  // 直接修改原对象

// 不可变方式
const original = { name: '张三' };
const updated = { ...original, name: '李四' };

// 数组不可变操作
const list = [1, 2, 3];
const newList = [...list, 4]; // 添加
const filtered = list.filter(n => n !== 2); // 删除

📌 使用不可变数据可以避免因共享状态导致的难以追踪的 bug,也是 Redux 等状态管理库的核心思想。

4. 高阶函数(Higher-Order Functions)

高阶函数是指至少满足下列条件之一的函数:接受一个或多个函数作为参数;返回一个函数。JavaScript 中常见的 map, filter, reduce 都是高阶函数。

// 自定义高阶函数
const withLog = (fn) => {
  return (...args) => {
    console.log(`调用参数: ${args}`);
    return fn(...args);
  };
};

const add = (a, b) => a + b;
const loggedAdd = withLog(add);
loggedAdd(3, 5); // 控制台输出参数,返回8

// 返回函数的工厂
const multiply = x => y => x * y;
const double = multiply(2);
double(10); // 20

🎯 高阶函数让抽象变得简单,例如用 filter 代替循环加判断,代码更声明式。

5. 柯里化(Currying)

柯里化是把一个多参数函数转换成嵌套的一元函数的过程,使得每个函数只接受一个参数。有助于创建部分应用函数。

// 普通多参数函数
const sum = (a, b, c) => a + b + c;

// 柯里化版本
const curriedSum = a => b => c => a + b + c;
curriedSum(1)(2)(3); // 6

// 实际应用:固定部分参数
const discount = price => discount => price * discount;
const tenPercentOff = discount(0.1);
tenPercentOff(100); // 10

// 通用的柯里化辅助函数
const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return (...next) => curried(...args, ...next);
    }
  };
};

⚡ 柯里化让你能够通过“部分应用”轻松构建专用函数,提高复用性。

6. 函数组合(Compose / Pipe)

函数组合是将多个函数合并成一个新函数,从右向左(compose)或从左向右(pipe)执行。组合的结果是:输入经过每个函数逐步转换。

// 组合 compose(从右向左)
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// 管道 pipe(从左向右)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

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

const shout = compose(repeat, exclaim, toUpper);
shout('hello'); // "HELLO! HELLO!"

const pipeShout = pipe(toUpper, exclaim, repeat);
pipeShout('hello'); // "HELLO! HELLO!"

🔗 组合是函数式设计的基石,让逻辑像管道一样清晰。

7. 函子(Functor)

函子是一个实现了 map 方法的数据结构,该方法对容器内的值应用给定的函数并返回一个新的函子。Array 就是最常见的函子。

// 自定义函子 Maybe(处理空值)
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));
  }
}

Maybe.of(10).map(x => x * 2).map(x => x + 1); // Maybe(21)
Maybe.of(null).map(x => x * 2); // Maybe(null) 安全

// 应用:避免空值错误
const safeUser = Maybe.of({ address: { city: '北京' } });
safeUser.map(u => u.address).map(a => a.city); // Maybe('北京')

📦 函子让你在不脱离上下文的情况下操作值,实现链式调用和错误处理。

8. 实际应用:声明式数据处理

结合 map、filter、reduce 和组合,可以写出清晰的数据转换流水线。

// 示例:从用户列表中提取成年人的姓名并排序
const users = [
  { name: '张三', age: 25 },
  { name: '李四', age: 17 },
  { name: '王五', age: 32 },
  { name: '赵六', age: 16 }
];

const result = users
  .filter(user => user.age >= 18)
  .map(user => user.name)
  .sort();

// result: ['张三', '王五']

// 用函数式组合重构
const filterAdult = users => users.filter(u => u.age >= 18);
const getName = u => u.name;
const sortByName = names => [...names].sort();

const getAdultNames = pipe(filterAdult, arr => arr.map(getName), sortByName);
getAdultNames(users); // ['张三', '王五']

✅ 组合让测试变得简单:每个小函数可独立测试,然后像乐高一样拼装。

9. 副作用与IO函子(概念)

纯函数不能有副作用,但真实应用需要读写 DOM、发送请求。函数式编程通过将副作用“装箱”到特定容器(如 IO 函子)来保持纯度,最后在 main 函数中执行。

// 示意:IO 函子(简化版)
class IO {
  constructor(effect) { this.effect = effect; }
  static of(value) { return new IO(() => value); }
  map(fn) { return new IO(() => fn(this.effect())); }
  run() { return this.effect(); }
}

// 读取输入并打印(纯描述)
const read = new IO(() => prompt('输入:'));
const program = read.map(s => `你输入了: ${s}`).map(console.log);
// 最后在程序入口运行:program.run();

🎭 将副作用延迟到边界执行,让核心逻辑保持纯净。

10. 性能与可访问性考量

函数式编程可能带来额外的内存开销(创建新对象),但现代引擎已经高度优化。合理使用不可变数据、记忆化(memoization)和尾调用优化可以提升性能。

// 记忆化示例
const memoize = (fn) => {
  const cache = {};
  return (arg) => cache[arg] || (cache[arg] = fn(arg));
};

const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n - 1));
factorial(5); // 计算并缓存
factorial(5); // 直接返回缓存结果

// 尾递归优化(需要严格模式)
const sum = (n, acc = 0) => n === 0 ? acc : sum(n - 1, acc + n);

💡 使用 prefers-reduced-motion 等媒体查询可减少动画,但在函数式代码中主要是减少不必要的计算。

📚 总结:函数式编程不是银弹,但它能帮助我们编写更可预测、更易测试和更模块化的代码。在 JavaScript 中采用函数式风格,可以从使用纯函数、避免突变开始,逐步引入高阶函数、组合和函子。配合现代工具(如 Redux、Ramda、lodash/fp),能极大提升开发效率和代码质量。