JavaScript 函数组合(Compose & Pipe)详解

JavaScript 函数组合(Compose & Pipe)详解

前言

当你学习 JavaScript 进阶时,一定会遇到函数组合 这个概念。它是函数式编程的核心思想之一,能让你像搭积木一样把小函数拼成大函数。

今天我们来彻底搞懂这段代码:

javascript 复制代码
const compose = (...functions) => (value) => {
    return functions.reduceRight((acc, fn) => fn(acc), value);
};

const pipe = (...functions) => (value) => {
    return functions.reduce((acc, fn) => fn(acc), value);
};

看起来很吓人?别怕,我们一层一层拆开。


一、先搞懂三个小函数

javascript 复制代码
const add5 = x => x + 5;       // 加 5
const multiply3 = x => x * 3;  // 乘 3
const subtract2 = x => x - 2;  // 减 2

它们各自做的事情非常简单:

javascript 复制代码
add5(10);       // 15
multiply3(15);  // 45
subtract2(45);  // 43

如果我们想依次执行这三步呢?

最笨的写法:嵌套调用

javascript 复制代码
subtract2(multiply3(add5(10)));
// 从最里面开始算:
// 第1步:add5(10)                    → 15
// 第2步:multiply3(15)               → 45
// 第3步:subtract2(45)               → 43

问题:当函数多了,嵌套会变成这样 👇

javascript 复制代码
// 😵 可读性极差
h(g(f(e(d(c(b(a(value))))))));

函数组合就是为了解决这个问题!


二、什么是函数组合?

函数组合 = 把多个小函数,合并成一个新函数。

就像工厂的流水线

复制代码
原料(10) → [加5] → [乘3] → [减2] → 成品(43)

每个工位(函数)只做一件简单的事,串起来就能完成复杂的任务。


三、compose 逐层拆解

完整代码

javascript 复制代码
const compose = (...functions) => (value) => {
    return functions.reduceRight((acc, fn) => fn(acc), value);
};

这一行代码包含了三个知识点,我们逐个击破。

知识点 1:剩余参数 ...functions

javascript 复制代码
const compose = (...functions) => { /* ... */ };

...functions 把所有传入的参数收集成一个数组

javascript 复制代码
compose(subtract2, multiply3, add5);
// functions = [subtract2, multiply3, add5]

知识点 2:返回一个新函数(柯里化)

javascript 复制代码
const compose = (...functions) => (value) => { /* ... */ };

compose 并不直接计算结果,而是返回一个新函数 ,这个新函数等待接收 value

javascript 复制代码
// 第一步:传入要组合的函数,得到一个新函数
const composed = compose(subtract2, multiply3, add5);
// composed 现在是一个函数:(value) => { ... }

// 第二步:传入具体的值,开始计算
composed(10); // 43

等价于:

javascript 复制代码
// compose(subtract2, multiply3, add5) 返回了这个:
const composed = (value) => {
    const functions = [subtract2, multiply3, add5];
    return functions.reduceRight((acc, fn) => fn(acc), value);
};

知识点 3:reduceRight 从右往左执行

javascript 复制代码
functions.reduceRight((acc, fn) => fn(acc), value);

reduceRightreduce 的区别:

方法 方向
reduce
reduceRight

完整执行过程

javascript 复制代码
const composed = compose(subtract2, multiply3, add5);
composed(10);

此时 functions = [subtract2, multiply3, add5]value = 10

reduceRight 从右往左遍历数组:

复制代码
functions 数组: [subtract2, multiply3, add5]
                                        ←←← 从右往左
轮次 acc(累加器) fn(当前函数) 计算 fn(acc) 结果
初始 10(value) --- --- 10
第 1 轮 10 add5 add5(10) 15
第 2 轮 15 multiply3 multiply3(15) 45
第 3 轮 45 subtract2 subtract2(45) 43

最终返回 43

图解

复制代码
compose(subtract2, multiply3, add5)(10)

执行方向:从右到左 ←←←

                 ←─────────────────────────
                 │                        │
            subtract2 ← multiply3 ← add5 ← 10
               │            │         │
               │            │       10+5=15
               │          15*3=45
             45-2=43

结果:43

四、pipe 逐层拆解

完整代码

javascript 复制代码
const pipe = (...functions) => (value) => {
    return functions.reduce((acc, fn) => fn(acc), value);
};

compose 唯一的区别 :用的是 reduce(从左往右),而不是 reduceRight

执行过程

javascript 复制代码
const piped = pipe(add5, multiply3, subtract2);
piped(10);

此时 functions = [add5, multiply3, subtract2]value = 10

reduce 从左往右遍历数组:

复制代码
functions 数组: [add5, multiply3, subtract2]
                 →→→ 从左往右
轮次 acc(累加器) fn(当前函数) 计算 fn(acc) 结果
初始 10(value) --- --- 10
第 1 轮 10 add5 add5(10) 15
第 2 轮 15 multiply3 multiply3(15) 45
第 3 轮 45 subtract2 subtract2(45) 43

最终返回 43

图解

复制代码
pipe(add5, multiply3, subtract2)(10)

执行方向:从左到右 →→→

  ─────────────────────────────→
  │                            │
  10 → add5 → multiply3 → subtract2
         │         │           │
       10+5=15   15*3=45    45-2=43

结果:43

五、compose vs pipe 对比

两者结果相同,但函数的书写顺序相反

javascript 复制代码
// compose:从右往左(像数学写法)
const composed = compose(subtract2, multiply3, add5);
//                       ←── ←── ←── 执行方向

// pipe:从左往右(像阅读顺序)
const piped = pipe(add5, multiply3, subtract2);
//                 ──→  ──→  ──→ 执行方向

两者都是对 10 执行:先加5,再乘3,最后减2

复制代码
                    compose                          pipe
              (subtract2, multiply3, add5)    (add5, multiply3, subtract2)
                    ←←← 从右往左                    →→→ 从左往右

书写顺序:         结果 ← 过程 ← 起点            起点 → 过程 → 结果
类比:             数学表达式 f(g(x))            流水线:x → g → f

该用哪个?

compose pipe
执行方向 右 → 左 左 → 右
阅读习惯 像数学公式 f(g(x)) 像自然语言,从上到下
使用场景 函数式编程传统写法 更直观,更多人喜欢

💡 推荐 :大多数场景下 pipe 更容易阅读,因为执行顺序和书写顺序一致。


六、为什么要用函数组合?

场景:处理用户数据

javascript 复制代码
// 一堆小函数,每个只做一件事
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const split = str => str.split(' ');
const join = arr => arr.join('-');

// ❌ 嵌套写法(难读)
const result = join(split(toLowerCase(trim("  Hello World  "))));

// ✅ pipe 写法(清晰!)
const slugify = pipe(trim, toLowerCase, split, join);
const result = slugify("  Hello World  ");
// "hello-world"

执行过程

复制代码
"  Hello World  "
       │
       ▼ trim
 "Hello World"
       │
       ▼ toLowerCase
 "hello world"
       │
       ▼ split
 ["hello", "world"]
       │
       ▼ join
  "hello-world"

好处

复制代码
✅ 每个函数只做一件事(单一职责)
✅ 函数可以复用(trim、toLowerCase 到处都能用)
✅ 像搭积木一样组合(需要新功能?加一个函数就行)
✅ 容易测试(每个小函数单独测试)
✅ 可读性强(一眼看出数据处理流程)

七、自己动手实现一遍

如果 reduce 让你头晕,可以先用 for 循环实现:

pipe 的 for 循环版本

javascript 复制代码
const pipe = (...functions) => (value) => {
    let result = value;

    for (let i = 0; i < functions.length; i++) {
        result = functions[i](result); // 把上一步的结果传给下一个函数
    }

    return result;
};

compose 的 for 循环版本

javascript 复制代码
const compose = (...functions) => (value) => {
    let result = value;

    for (let i = functions.length - 1; i >= 0; i--) { // 从后往前
        result = functions[i](result);
    }

    return result;
};

是不是一下就理解了?reducereduceRight 只是上面 for 循环的简写而已。


八、总结

javascript 复制代码
// compose:从右往左执行
const compose = (...fns) => (val) => fns.reduceRight((acc, fn) => fn(acc), val);

// pipe:从左往右执行
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);
概念 说明
函数组合 把多个小函数合并成一个大函数
compose 从右往左执行,像数学写法
pipe 从左往右执行,像流水线
核心思想 上一个函数的输出 = 下一个函数的输入
reduceRight 从数组最后一项开始归纳
reduce 从数组第一项开始归纳
柯里化 先传函数,返回新函数,再传值

一句话总结:函数组合就是搭建数据的流水线------数据从一头进去,经过层层加工,从另一头出来。 🏭

逐层拆解 const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val)

这一行代码看着很唬人,其实它用了三个语法叠加在一起。我们一个一个拆。


语法一:箭头函数

javascript 复制代码
// 普通函数
function add(a, b) {
    return a + b;
}

// 箭头函数(简写)
const add = (a, b) => a + b;

当箭头函数只有一个表达式 时,可以省略 {}return

javascript 复制代码
// 这两种写法完全等价
const double = (x) => { return x * 2; };
const double = x => x * 2;

语法二:返回函数的函数(柯里化)

一个函数可以返回另一个函数

javascript 复制代码
// 普通写法
function greet(greeting) {
    return function(name) {
        return greeting + ', ' + name;
    };
}

// 箭头函数写法
const greet = (greeting) => (name) => greeting + ', ' + name;

怎么用?

javascript 复制代码
// 第一次调用:传入 greeting,返回一个新函数
const sayHello = greet('你好');
// sayHello 现在是:(name) => '你好' + ', ' + name

// 第二次调用:传入 name,得到最终结果
sayHello('张三');  // '你好, 张三'
sayHello('李四');  // '你好, 李四'

// 也可以连着调用
greet('你好')('张三');  // '你好, 张三'

一步步理解连续箭头

javascript 复制代码
const a = (x) => (y) => (z) => x + y + z;

这其实是三层函数嵌套

javascript 复制代码
// 等价于:
function a(x) {
    return function(y) {
        return function(z) {
            return x + y + z;
        };
    };
}

调用方式:

javascript 复制代码
a(1)(2)(3);  // 6

// 拆开看:
const step1 = a(1);       // (y) => (z) => 1 + y + z
const step2 = step1(2);   // (z) => 1 + 2 + z
const step3 = step2(3);   // 1 + 2 + 3 = 6

语法三:剩余参数 ...

javascript 复制代码
const test = (...args) => {
    console.log(args);
};

test(1, 2, 3);       // [1, 2, 3]
test('a', 'b');       // ['a', 'b']

...args 把所有传入的参数收集成一个数组


现在拆解 pipe

原始代码

javascript 复制代码
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);

第一步:展开成普通函数

javascript 复制代码
function pipe(...fns) {
    // fns 是一个数组,比如 [add5, multiply3, subtract2]

    return function(val) {
        // val 是传入的初始值,比如 10

        return fns.reduce(function(acc, fn) {
            return fn(acc);
        }, val);
    };
}

第二步:用 for 循环替代 reduce

javascript 复制代码
function pipe(...fns) {
    return function(val) {
        let result = val;  // 从初始值开始

        for (let i = 0; i < fns.length; i++) {
            result = fns[i](result);  // 把上一步结果传给下一个函数
        }

        return result;
    };
}

第三步:看看实际调用

javascript 复制代码
const add5 = x => x + 5;
const multiply3 = x => x * 3;
const subtract2 = x => x - 2;

// 第一次调用:传入三个函数
const piped = pipe(add5, multiply3, subtract2);
// 此时 fns = [add5, multiply3, subtract2]
// piped 是一个函数:(val) => { ... }

// 第二次调用:传入值
piped(10);
// val = 10
// 开始执行 reduce...

第四步:模拟 reduce 的执行

复制代码
fns = [add5, multiply3, subtract2]
初始值 val = 10

轮次    acc(累积值)    fn(当前函数)    fn(acc)       新的acc
─────────────────────────────────────────────────────────
 1       10           add5          add5(10)        15
 2       15           multiply3     multiply3(15)   45
 3       45           subtract2     subtract2(45)   43

最终返回:43

用一张图理解全貌

复制代码
pipe(add5, multiply3, subtract2)(10)
 │                                │
 │ 第一次调用                      │ 第二次调用
 │                                │
 ▼                                ▼
(...fns) =>                    (val) => fns.reduce(...)
fns = [add5, multiply3, subtract2]   val = 10
                │
                ▼
        reduce 开始流水线:

        10 ──→ add5 ──→ multiply3 ──→ subtract2 ──→ 43
           +5=15      *3=45         -2=43

再来几个例子加深理解

例子 1:最简单的 ------ 只有一层箭头

javascript 复制代码
const double = x => x * 2;
double(5); // 10

例子 2:两层箭头

javascript 复制代码
const add = a => b => a + b;

add(3)(4);  // 7

// 等价于
function add(a) {
    return function(b) {
        return a + b;
    };
}

例子 3:两层箭头 + 剩余参数

javascript 复制代码
const sum = (...nums) => (multiplier) => {
    const total = nums.reduce((acc, n) => acc + n, 0);
    return total * multiplier;
};

sum(1, 2, 3)(2);
// nums = [1, 2, 3], multiplier = 2
// total = 6
// 6 * 2 = 12

例子 4:就是 pipe

javascript 复制代码
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);

// 拆开:
const pipe = (...fns) => {
    // 返回一个新函数
    return (val) => {
        // 从左到右依次执行每个函数
        return fns.reduce((acc, fn) => {
            return fn(acc);
        }, val);
    };
};

总结

这一行代码:

javascript 复制代码
const pipe = (...fns) => (val) => fns.reduce((acc, fn) => fn(acc), val);

其实就是三个简单语法叠在一起

语法 在代码中的位置 作用
箭头函数 => 整行都在用 简写函数
剩余参数 ...fns (...fns) 收集所有传入的函数为数组
返回函数(柯里化) => (val) => 第一次传函数,第二次传值
reduce .reduce((acc, fn) => fn(acc), val) 依次执行每个函数

🎯 核心思想 :看到连续的 =>,就知道这是"返回函数的函数",需要分多次调用 。第一个 () 传第一批参数,第二个 () 传第二批参数,以此类推。

后记

2026年4月16日12点42分于上海,在opus 4.6辅助下完成。

相关推荐
lly2024062 小时前
Python uWSGI 安装配置
开发语言
两年半的个人练习生^_^2 小时前
每日一学:设计模式之原型模式
java·开发语言·设计模式·原型模式
elseif1232 小时前
初学者必背【考点清单(大全)】【上篇】
开发语言·c++·笔记·学习·循环结构·分支结构·考纲
并不喜欢吃鱼2 小时前
从零开始C++----二.(下篇)模版进阶与编译全过程的复习
开发语言·c++
23471021272 小时前
4.17 学习笔记
开发语言·软件测试·笔记·python·学习
heimeiyingwang2 小时前
【架构实战】Docker容器网络模型详解
网络·docker·架构
不知名的老吴2 小时前
View的三大特性之一:迟绑定
开发语言·c++·算法
深邃-3 小时前
【Web安全】-基础环境安装:虚拟机安装,JDK环境安装(1)
java·开发语言·计算机网络·安全·web安全·网络安全·安全架构
前端老石人3 小时前
前端网站换肤功能的 3 种实现方案
开发语言·前端·css·html