文章

函数式编程思想:纯函数、柯里化与偏函数

面试高频三连问:什么是纯函数?柯里化和偏函数到底有什么区别?函数式编程到底在解决什么问题、业务里怎么用。

函数式编程思想:纯函数、柯里化与偏函数

一句话概括

函数式编程(Functional Programming,简称 FP)不是”多写几个函数”,而是一种编程范式:把”怎么做一件事”写成”一个个函数的组合”。它有三个核心信条:函数是一等公民(函数能当参数、能当返回值)、纯函数(相同输入永远相同输出、无副作用)、数据不可变(不修改外部状态)。

面试官爱问它,是因为它直接对应你每天都在用的东西:Array.map/filter/reduce 是高阶函数,Redux 的 reducer 必须是纯函数,事件传参常写成柯里化形式。能讲清”纯函数 + 柯里化 + 偏函数”这三个概念,基本就过线了。

核心知识点

1. 纯函数:FP 的基石

结论:纯函数 = 相同输入永远得到相同输出 + 没有任何”可观察的副作用”。

说白了,一个函数只看你传给它的参数,不碰任何外部的东西,也不改写任何外部的东西,那它就是纯的。常见副作用包括:改全局变量、改外部对象、操作 DOM、发请求、写文件、调用 Date.now() / Math.random()、打 console.log(严格意义上也算,但工程上一般放宽)。

1
2
3
4
5
6
7
8
9
10
11
// ❌ 不纯:依赖外部变量,taxRate 一变结果就变
let taxRate = 0.1;
const calc = (price) => price * (1 + taxRate);

// ❌ 不纯:改了外部状态(cache)
const cache = {};
const fetchPrice = (id) => { cache[id] = 1; };

// ✅ 纯:只看参数,不改世界,结果永远可预测
const addTax = (price, rate) => price * (1 + rate);
addTax(100, 0.1); // 永远是 110

纯函数的价值(面试必说三点):

优点原因
可缓存(记忆化)相同输入 = 相同输出,缓存结果不会错
易测试不依赖环境、不污染状态,给参断言即可
易并行无共享状态,多线程/并发改数据不怕竞态

一个好记的直觉:Array.slice() 是纯函数(切出来新数组,不改原数组),Array.splice() 是不纯的(会改原数组)。

2. 柯里化:把多参函数拆成一连串单参函数

结论:柯里化把一个”接收多个参数的函数”转换成”每次只接收一个参数、返回一个新函数,直到参数凑齐才真正执行”的链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
// 普通写法:一次传完
const add = (a, b, c) => a + b + c;
add(1, 2, 3); // 6

// 柯里化:每次传一个,返回新函数
const curriedAdd = (a) => (b) => (c) => a + b + c;
curriedAdd(1)(2)(3); // 6

// 关键点:可以先固定一部分参数,得到更专用的函数(参数复用)
const add10 = curriedAdd(10);
add10(2)(3);  // 15
add10(5)(6);  // 21

背后的原理就是闭包:每次调用都把已传的参数”记住”,返回一个等待剩余参数的函数。

3. 偏函数(部分应用):一次固定”多个”参数

结论:偏函数是”先喂给函数一部分参数,返回一个新的、参数更少的函”,剩下的参数后面再传。

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 每次都重复写相同的 'INFO'
log('INFO', 'user login');
log('INFO', 'order created');

// ✅ 偏函数:把 level 固定住,生成专用函数
const logInfo = partial(log, 'INFO');
logInfo('user login');
logInfo('order created');

function partial(fn, ...preset) {
  return (...rest) => fn(...preset, ...rest);
}

注意:Function.prototype.bind 本身就是典型的偏应用——add.bind(null, 1) 就是把第一个参数固定成 1,返回一个新函数。

4. 柯里化 vs 偏函数:面试必问辨析

这是最高频的混淆点,必须分清:

维度柯里化(Currying)偏函数(Partial Application)
每次传参只能 1 个(逐个收集)一次可固定多个
返回一连串单参函数,直到参数齐一个处理”剩余参数”的函数
灵活性更”碎”,可任意分步相对固定,一次定好预设参
典型场景分步收集、参数复用、函数组合固定常用参数、简化调用

一句话记忆:柯里化是”每次喂一口”,偏函数是”一次喂一勺”;柯里化一定是单参链式,偏函数可以一次固定多个。

5. 为什么面试爱问:真实价值

  • 参数复用:把不变的参数先固定(如 baseURL、log 级别),后面只传变化的。
  • 延迟执行:柯里化返回的函数可以存着,等条件齐了再调用。
  • 函数组合的基础:先把 map/filter 柯里化,才能把它们像积木一样组合(见下一篇”手写 compose”)。
  • 可预测性:纯函数让状态变化可追踪,是 Redux、Immer、React 渲染正确的底层保障。

其实你每天都在用

  • Redux 的 reducer 必须是纯函数:相同 action 永远产出相同 state,才能放心地时间旅行调试、热重载。
  • React 事件传参:常写成 id => event => ... 的柯里化形式,先固定 id 再接事件对象。
  • lodash 的 _.curry / _.partial:一行把普通函数变成柯里化/偏函数版本。
  • 数组方法本身就是函数式:map/filter/reduce 接收函数当参数(高阶函数),链式调用就是数据流的”管道”。
  • 请求封装:axios.create({ baseURL }) 本质就是偏函数——把 baseURL 固定,返回一个专用请求器。
  • Ramda 库:所有函数默认柯里化,天然适合组合式编程。

常见误解(FAQ)

❌ 误区一:”函数式编程就是多写几个函数”

错。FP 的核心是”函数是一等公民 + 纯函数 + 数据不可变”,不是”代码里出现函数就算函数式”。setTimeout(fn, 0) 也用了函数,但它一点都不函数式。真正的函数式是让函数可组合、结果可预测。

❌ 误区二:”柯里化和偏函数是一回事”

这是面试最高频失分点。区别在于每次传几个参数:柯里化严格”每次一个、返回单参函数序列”;偏函数”一次可以固定多个参数、返回一个处理剩余参数的函数”。两者都能做”参数复用”,但机制不同。

❌ 误区三:”纯函数连 console.log 都不能有,太不实用了”

工程上”纯”主要指引用透明 + 不改外部状态。console.log 这种”不影响结果、只是打印”的副作用,在绝大多数团队的纯函数约定里是被放过的。真正的红线是:不能改外部变量、不能依赖会变的外部环境(如 Date.now()、Math.random())。

❌ 误区四:”用了 map/filter 就是函数式,纯不纯无所谓”

不纯的函数一旦进组合链会很危险。比如 filter 的回调里偷偷改了外部数组,你就没法预测这段数据流的结果,出了 bug 极难溯源。函数式的收益正是建立在”每一步都是纯的”之上。

一句话总结

函数式编程的核心是”纯函数 + 一等公民函数“——柯里化是单参链式收集、偏函数是多参一次固定,两者都是为了让函数更好复用、更好组合,而组合正是函数式处理复杂数据流的主武器。

本文由作者按照 CC BY 4.0 进行授权