人人都会AI编程

9.6 函数柯里化、偏函数与函数组合

更新时间:2026-07-11

函数式编程将函数视为构建程序的基石,而不仅仅是代码块。当函数可以像普通值一样被传递、返回和组合时,代码的表达力和复用性会迈上一个新台阶。这一节的核心概念——高阶函数、柯里化、偏函数与函数组合——正是解锁这种力量的关键。

28.2.1 高阶函数:接受或返回函数的函数

在 JavaScript 中,函数是一等公民,这意味着满足以下任一条件的函数就是高阶函数

  • 函数可以接受另一个函数作为参数
  • 函数可以返回一个函数

你其实每天都在使用高阶函数。数组的 mapfilterreduce,事件监听器的 addEventListener,Promise 的 then ——它们都接收一个回调函数作为参数,都是高阶函数。

// 接受函数作为参数:Array.prototype.map
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // [2, 4, 6]

// 返回一个函数:创建一个可配置的处理函数
function createMultiplier(multiplier) {
  return function (value) {
    return value * multiplier;
  };
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

实际应用

  • 抽象通用逻辑:高阶函数能封装重复的控制流程。比如,可以写一个 retry 函数接收操作和重试次数,内部处理错误与重试逻辑。
  • 延迟执行:返回函数的特性让我们可以构造可复用的函数,而不是立即执行。React 的事件处理程序常常需要这种模式(如 onClick={() => handler(id)})。
  • 依赖注入:将外部依赖(如日志、数据库操作)作为函数参数传入,方便测试和替换。

高阶函数让代码从“描述具体步骤”升级为“组装行为模块”,这正是函数式风格的基础。

28.2.2 柯里化:将多参数函数转化为单参数函数链

柯里化(Currying) 是指将一个接收多个参数的函数转化为一系列每次只接收一个参数的函数。每接收一个参数,就返回一个新函数,直到所有参数收集完整,才执行原始函数并返回结果。

// 普通函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化版本(手动实现)
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

console.log(curriedAdd(1)(2)(3)); // 6

通常我们会写一个通用的 curry 辅助函数来自动化这个过程:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}

const curriedAdd = curry((a, b, c) => a + b + c);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6(也可以一次传入多个参数)

柯里化的实际价值

  • 参数复用:固定部分参数,生成更专用的函数。例如,从通用的日志函数柯里化出 errorLog = curriedLog('error')
  • 延迟执行:直到所有参数收集完毕才真正执行,这在处理事件流或配置链式调用时特别有用。
  • 函数组合的铺垫:柯里化后,每个函数都只接收一个参数,非常适合进行函数组合(见下面28.2.4)。

不过要注意,过度柯里化可能让代码变得晦涩。在团队中,应权衡可读性,尤其在参数个数不固定时,柯里化未必是最佳选择。

28.2.3 偏函数:预设部分参数的函数

偏函数(Partial Application) 与柯里化有相似之处,但更直接:它固定一个函数的部分参数,返回一个新函数,新函数接收剩余的参数并执行。偏函数不要求每次只接收一个参数,它可以一次固定多个参数。

// 偏函数辅助函数
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn.apply(this, presetArgs.concat(laterArgs));
  };
}

function greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`;
}

const sayHelloTo = partial(greet, 'Hello');
console.log(sayHelloTo('Alice', '!')); // Hello, Alice!

与柯里化的区别

  • 柯里化强制将每个参数拆分成单独的函数调用,偏函数则一次性固定多个参数。
  • 柯里化返回的是一系列一元函数的链,偏函数返回的是一个期望剩余参数的新函数。
  • 偏函数更像一个“配置好的版本”,在实际开发中更容易被采纳,因为它更灵活也更直觉。

实际应用

  • 创建专用函数:比如固定请求 fetchWithDefaults,预设 baseURL 或 headers。
  • 事件处理const onClick = partial(saveData, userId, data),在调用时仅传入必要的新参数。
  • 减少样板代码:当某个函数的某些参数在模块内总是一样时,用偏函数生成一个“快捷版”,避免重复传递相同参数。

28.2.4 函数组合:将多个函数串联成数据管道

函数组合(Compose) 的概念很简单:如果一个函数的输出类型与另一个函数的输入类型匹配,就可以将它们组合成一个新的函数,数据从左流到右(或从右到左)经过一系列转换。

// 从右向左的组合函数(通常称为 compose)
function compose(...fns) {
  return function(value) {
    return fns.reduceRight((acc, fn) => fn(acc), value);
  };
}

// 从左向右的流水线函数(通常称为 pipe)
function pipe(...fns) {
  return function(value) {
    return fns.reduce((acc, fn) => fn(acc), value);
  };
}

示例:处理一个字符串,先去除空格,再转为大写,最后添加叹号。

const trim = str => str.trim();
const toUpper = str => str.toUpperCase();
const exclaim = str => str + '!';

// 从右向左:先 trim,再 toUpper,最后 exclaim
const shout = compose(exclaim, toUpper, trim);
console.log(shout('  hello  ')); // "HELLO!"

// 从左向右更易阅读
const shoutPipe = pipe(trim, toUpper, exclaim);
console.log(shoutPipe('  hello  ')); // "HELLO!"

函数组合的核心价值

  • 消除中间变量:不再需要临时变量存储每一步的结果,代码变成清晰的数据流。
  • 高内聚、低耦合:每个函数只做一件事,可单独测试和复用,组合后就获得新的能力。
  • 声明式编程:代码更贴近“做什么”而不是“怎么做”。

在实际开发中,像 Lodash 的 _.flow、Redux 的 compose、React 的高阶组件组合,都体现了函数组合的思想。

必须满足的组合条件
函数组合的关键是类型匹配:前一个函数的返回值的类型必须与下一个函数的参数类型一致。当某个函数可能返回 null 或期望不同结构时,组合链就会断裂。此时需要引入 Maybe、Either 等容器来处理异常,这也就是下一节要讲的函子(Functor)和 Monad 的用武之地。

28.2.5 综合运用:从数据到视图的管道

将这些概念串联起来,可以构造出高度可读、可维护的代码。假设有一个用户列表,需要筛选出活跃用户,提取姓名并格式化为欢迎语句:

const users = [
  { name: 'Alice', active: true },
  { name: 'Bob', active: false },
  { name: 'Charlie', active: true }
];

const filterActive = users => users.filter(u => u.active);
const pluckName = users => users.map(u => u.name);
const greet = name => `Hello, ${name}!`;

// 偏函数固定欢迎前缀
const welcome = partial(greet, 'Welcome');

const activeUserGreetings = pipe(filterActive, pluckName, names => names.map(welcome));
console.log(activeUserGreetings(users));
// ["Welcome, Alice!", "Welcome, Charlie!"]

这个例子用到了高阶函数(map/filter)、偏函数(welcome)和函数组合(pipe)。每一步都像积木一样可以独立测试和替换;整个数据转换过程一目了然。

掌握这些函数式工具,不是为了炫耀技巧,而是为了让代码的意图更清晰、组合更方便、变更更从容。当逻辑变得复杂时,用纯函数和组合的方式构建系统,往往能比命令式的循环和条件分支更容易维护,也更不容易出错。