函数式编程(Functional Programming,FP)是一种以函数为核心的编程范式,强调通过纯函数、不可变数据、函数组合等方式构建程序,而非依赖状态修改和指令式流程。与传统的命令式编程(一步步告诉计算机 “怎么做”)不同,函数式编程更关注 “做什么”,追求代码的简洁、可复用、可测试性。

一、先搞懂:什么是 JS 函数式编程?

先通过命令式 vs 函数式的对比示例,直观感受核心差异:

场景:筛选数组中大于 10 的偶数并翻倍

  1. 命令式编程(传统写法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 命令式:关注“步骤”,手动管理状态和循环
const arr = [1, 12, 5, 20, 7, 18];
const result = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
// 步骤1:筛选大于10的数
if (item > 10) {
// 步骤2:筛选偶数
if (item % 2 === 0) {
// 步骤3:翻倍并存入结果
result.push(item * 2);
}
}
}
console.log(result); // [24, 40, 36]
  1. 函数式编程(FP 写法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数式:关注“结果”,用纯函数组合实现逻辑
const arr = [1, 12, 5, 20, 7, 18];

// 1. 定义纯函数(无副作用、输入决定输出)
const isGreaterThan10 = (num) => num > 10;
const isEven = (num) => num % 2 === 0;
const double = (num) => num * 2;

// 2. 用数组高阶函数(map/filter)组合逻辑
const result = arr
.filter(isGreaterThan10)
.filter(isEven)
.map(double);

console.log(result); // [24, 40, 36]

函数式编程的核心特征(从示例中提炼)

  1. 纯函数(Pure Function):
  • 输入相同,输出必然相同(无随机值、无依赖外部状态);
  • 无副作用(不修改外部变量、不操作 DOM、不发请求);
  • 示例中 isGreaterThan10/isEven/double 都是纯函数。
  1. 不可变数据(Immutable Data):
  • 不修改原始数据,而是返回新数据(示例中 filter/map 均返回新数组,原数组 arr 未被修改);
  • JS 中可通过 Object.freeze、展开运算符 ...Object.assign 实现简单不可变。
  1. 高阶函数(Higher-Order Function):
  • 接受函数作为参数,或返回函数的函数(示例中 filter/map 是高阶函数,接收纯函数作为参数);
  • JS 内置高阶函数:map/filter/reduce/forEach/sort 等。
  1. 函数组合(Function Composition):
    将多个小函数组合成一个大函数,实现复杂逻辑(示例中 filter+filter+map 就是简单的组合)。

二、深入实践:函数式编程的核心用法

  1. 纯函数:避免副作用

反例(不纯函数):依赖外部变量、修改外部状态

1
2
3
4
5
6
7
8
let globalNum = 10;
// 不纯:依赖外部变量,输入相同但输出可能不同
const add = (num) => num + globalNum;
// 不纯:修改外部数组(副作用)
const pushItem = (arr, item) => {
arr.push(item);
return arr;
};

正例(纯函数):

1
2
3
4
5
6
7
8
// 纯函数:输入决定输出,无外部依赖
const add = (num1, num2) => num1 + num2;
// 纯函数:返回新数组,不修改原数组
const pushItem = (arr, item) => [...arr, item];

// 测试:输入相同,输出必相同
console.log(add(2, 3)); // 5(永远)
console.log(pushItem([1,2], 3)); // [1,2,3](原数组仍为[1,2])
  1. 不可变数据:避免状态混乱
1
2
3
4
5
6
7
8
9
10
11
const user = { name: "张三", age: 20 };

// 命令式:修改原对象(副作用)
user.age = 21;
console.log(user); // { name: "张三", age: 21 }

// 函数式:返回新对象,原对象不变
const updatedUser = { ...user, age: 21 };
console.log(user); // { name: "张三", age: 20 }(原对象未变)
console.log(updatedUser); // { name: "张三", age: 21 }

  1. 函数组合:封装复杂逻辑
    手动实现 “函数组合” 工具,将多个单职责函数组合成一个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 组合函数:从右到左执行(先执行最后一个函数,结果传给前一个)
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

// 复用之前的纯函数
const isGreaterThan10 = (num) => num > 10;
const isEven = (num) => num % 2 === 0;
const double = (num) => num * 2;

// 组合成新函数:先筛选>10 → 筛选偶数 → 翻倍
const processNum = compose(double, isEven, isGreaterThan10); // 注意顺序(从右到左)

// 扩展:数组处理的组合函数
const processArr = (arr) => arr.filter(compose(isEven, isGreaterThan10)).map(double);
console.log(processArr([1, 12, 5, 20, 7, 18])); // [24, 40, 36]
  1. 柯里化(Currying):函数参数复用
    柯里化是将多参数函数转换为单参数函数的过程,核心是 “参数复用”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 柯里化工具函数
const 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 add = (a, b, c) => a + b + c;
// 柯里化后
const curriedAdd = curry(add);

// 用法1:分步传参(复用参数)
const add5 = curriedAdd(5); // 固定第一个参数为5
const add5And6 = add5(6); // 固定第二个参数为6
console.log(add5And6(7)); // 5+6+7=18

// 用法2:直接传所有参数
console.log(curriedAdd(1,2,3)); // 6

三、为什么要用函数式编程?

  1. 代码更简洁、易读
    摒弃冗长的循环、条件判断嵌套,用 “函数组合” 替代 “步骤式指令”;
    单职责函数语义清晰(如 isEven/double 一看就懂,无需注释)。

  2. 易测试、易调试
    纯函数无依赖、无副作用,测试时只需关注 “输入→输出”,无需模拟外部状态;
    调试时可直接单独运行某个纯函数,定位问题更高效。

1
2
3
4
5
// 测试纯函数(无需任何环境准备)
test("isEven should return true for even numbers", () => {
expect(isEven(2)).toBe(true);
expect(isEven(3)).toBe(false);
});
  1. 避免状态混乱
    不可变数据杜绝了 “意外修改共享状态” 的问题(多线程 / 异步场景下尤为重要);
    例如 React 中,state 必须通过setState 修改(不可变思想),避免组件状态异常。

  2. 代码复用性更高
    纯函数、柯里化、函数组合可快速封装通用逻辑(如表单验证、数据处理);
    例如:封装一个通用的 “数据筛选 + 转换” 函数,可复用在多个业务场景。

四、函数式编程的优点与局限

优点 具体说明
可读性高 语义化函数名替代指令式步骤,代码接近自然语言
可维护性强 单职责函数低耦合,修改一个函数不影响其他逻辑
可测试性好 纯函数无依赖,单元测试无需复杂的环境模拟
并行安全 不可变数据避免多线程 / 异步场景下的 “竞态条件”(JS 中主要体现在异步代码)
复用性高 柯里化、组合函数可快速封装通用逻辑
局限 具体说明
性能开销 不可变数据会创建大量新对象(如频繁修改大数组 / 对象时,内存占用高于命令式)
学习成本高 柯里化、组合、函子等概念较抽象,新手难理解
不适合所有场景 简单的 “一次性逻辑”(如临时遍历数组)用函数式可能比命令式更繁琐
调试难度增加 多层函数组合时,定位问题需要跟踪函数执行链(可通过中间件 / 日志改善)

五、使用建议

  1. 不要 “一刀切”:混合范式
    JS 是多范式语言,无需完全抛弃命令式,按需使用函数式;
    例如:简单的循环用 for 没问题,复杂的数据处理用 filter/map/reduce,状态管理用不可变思想。

  2. 优先使用内置高阶函数
    优先用 map/filter/reduce/find 等内置函数,避免手动写循环;
    例如:reduce 可替代大部分 “遍历 + 累加 / 聚合” 逻辑,代码更简洁。

  3. 合理使用工具库(避免重复造轮子)
    简单场景:手写柯里化、组合函数;
    复杂场景:使用成熟的函数式库(如 lodash/fp、ramda),内置了大量纯函数、柯里化、组合工具。

1
2
3
4
5
6
7
8
9
10

// lodash/fp 示例(自动柯里化、不可变)
import fp from "lodash/fp";

const processArr = fp.flow(
fp.filter(fp.gt(fp.__, 10)), // __ 是占位符
fp.filter(fp.even),
fp.map(fp.multiply(2))
);
console.log(processArr([1, 12, 5, 20, 7, 18])); // [24, 40, 36]
  1. 避免过度抽象
    不要为了 “函数式” 而强行柯里化 / 组合,简单逻辑保持简洁;
    例如:const add = (a,b) => a+b 比柯里化后的 curriedAdd(1)(2) 更易读。

  2. 结合框架使用
    React:函数组件 + Hooks(纯函数思想)、Redux(不可变状态)都是函数式的典型应用;
    Vue:Pinia/Vuex 中的状态修改需通过 action/mutation(不可变思想)。

六、总结

函数式编程的核心是用纯函数构建无副作用的逻辑,它不是 “银弹”,但能显著提升代码的可维护性、可测试性。在 JS 中,我们无需精通所有函数式概念(如函子、单子),只需掌握 “纯函数、不可变、高阶函数、函数组合” 这几个核心,就能在实际开发中受益。

最终建议:以 “解决问题” 为核心,将函数式思想融入日常开发,而非机械套用范式 —— 简单逻辑保持直观,复杂逻辑用函数式简化,这才是 JS 函数式编程的最佳实践。