函数式编程(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 等方式实现。
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)执行。组合的结果是:输入经过每个函数逐步转换。
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 就是最常见的函子。
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 函数中执行。
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),能极大提升开发效率和代码质量。