← 返回题目列表

如何手写 compose 和 pipe 函数组合?

高频 中等 第 5 / 27 题 更新于 2026/07/29
手写代码函数式编程composepipe

简化版

compose 是从右到左组合函数,pipe 是从左到右组合函数。手写核心是把多个一元函数按顺序串起来,并正确处理初始参数、多参数入口和异步函数场景。

详细版

同步版可以用 reduceRightreduce

const compose = (...fns) => input =>
  fns.reduceRight((value, fn) => fn(value), input)

const pipe = (...fns) => input =>
  fns.reduce((value, fn) => fn(value), input)

更实用的版本需要允许第一个函数接收多个参数,其余函数接收前一个函数的返回值。异步场景则要从 Promise.resolve 开始串联,保证每一步等待上一项完成。

完整版教学

一、组合解决的是什么问题

没有组合时,多步数据处理常写成嵌套调用:

const result = trim(toLowerCase(removeSpace(input)))

调用顺序需要从里往外读,步骤一多就很难扫。组合函数把“上一步输出作为下一步输入”的模式抽出来,让数据处理链条变成可复用的流水线。

compose(a, b, c)(x): x -> c -> b -> a
pipe(a, b, c)(x):    x -> a -> b -> c

记忆钩子:compose 像数学函数 f(g(x)),从右往左;pipe 像管道流水,从左往右。

二、同步 compose 的基础实现

function compose(...fns) {
  return function composed(input) {
    return fns.reduceRight((value, fn) => fn(value), input)
  }
}

const add1 = x => x + 1
const double = x => x * 2
const result = compose(double, add1)(3) // double(add1(3)) = 8

reduceRight 的累计值就是前一个函数的返回值。假设输入是 3,先进入 add1 得到 4,再进入 double 得到 8。

三、同步 pipe 的基础实现

function pipe(...fns) {
  return function piped(input) {
    return fns.reduce((value, fn) => fn(value), input)
  }
}

const result = pipe(add1, double)(3) // double(add1(3)) = 8

pipe 的优势是阅读方向贴近执行方向,适合业务数据加工。compose 更贴近函数式库和数学表达,适合把小函数组合成更高阶的变换。

对比项composepipe
执行方向从右到左从左到右
阅读习惯像嵌套函数像流水线
常见场景Redux 中间件组合数据清洗链路

四、支持第一个函数多参数

基础版只支持单个输入,但很多入口函数需要多个参数:

function compose(...fns) {
  return function (...args) {
    if (fns.length === 0) return args[0]
    const last = fns[fns.length - 1]
    let index = fns.length - 2
    let result = last.apply(this, args)
    while (index >= 0) {
      result = fns[index].call(this, result)
      index -= 1
    }
    return result
  }
}

const add = (a, b) => a + b
const square = x => x * x
compose(square, add)(2, 3) // 25

约定通常是“最右侧函数接收原始多参数,左侧函数都是一元函数”。如果每个步骤都允许多参数,组合关系就不再清晰,调用者也难以推断下一个函数会收到什么。

五、异步组合怎么写

function pipeAsync(...fns) {
  return function (input) {
    return fns.reduce(
      (promise, fn) => promise.then(value => fn(value)),
      Promise.resolve(input),
    )
  }
}

const getUser = id => fetch(`/api/users/${id}`).then(r => r.json())
const pickName = user => user.name

异步版本的累计值是 Promise,而不是普通值。这样每一步都会等待上一项完成,任意一步 throw 或 reject,整条链都会进入 rejected。

六、空函数列表和类型校验

如果没有传函数,常见做法是返回恒等函数:

const identity = x => x
compose()(3) // 3

生产版本还应检查每个参数是否为函数。否则错误会推迟到运行时深处,调用栈不直观。面试写法可以简化,但要能说明这些边界。

七、常见误区与追问

  • 误区:composepipe 只是名字不同。 它们的执行方向相反,参数排列习惯不同。
  • 误区:所有函数都能自然接收多个参数。 通常只有入口函数接收多参数,中间步骤接收单个上一步结果。
  • 误区:异步函数可以直接用同步 reduce 返回 Promise 后下一步会收到 Promise 对象,而不是解析后的值。
  • 追问:空组合返回什么? 通常返回恒等函数,保持组合律。
  • 追问:为什么要保留 this 若组合对象方法,丢失上下文会导致方法内部访问失败。
  • 追问:错误如何传播? 同步版直接 throw,异步版通过 Promise 链 reject。

八、加强记忆

函数组合的本质是“把嵌套调用拉平成链”。compose 从右往左,像 f(g(x))pipe 从左往右,像数据穿过管道。手写时先实现同步一元链,再补入口多参数、空函数列表、类型校验和异步 Promise 串联,层次就很清楚。