如何用 JavaScript 实现一个 compose 函数 (函数组合)?

各位靓仔靓女,晚上好!我是你们的老朋友,今天咱不聊风花雪月,就来啃啃函数式编程里一个相当重要,但又经常被包装得高深莫测的家伙 —— compose 函数。说白了,它就是个函数“串串香”,把一堆函数串起来执行,让代码变得更优雅、更可读。

咱们先来个暖场小故事:

想象一下,你要做一份豪华三明治:

  1. 首先,你要把面包烤一下 (toastBread 函数)。
  2. 然后,在面包上抹上黄油 (spreadButter 函数)。
  3. 接着,放上火腿和奶酪 (addHamAndCheese 函数)。
  4. 最后,盖上另一片面包 (closeSandwich 函数)。

按照传统的方式,你可能会这样写:

const bread = "面包";
const toastedBread = toastBread(bread);
const butteredBread = spreadButter(toastedBread);
const sandwichWithHamAndCheese = addHamAndCheese(butteredBread);
const finalSandwich = closeSandwich(sandwichWithHamAndCheese);

console.log(finalSandwich); // "盖上面包的火腿奶酪黄油烤面包"

看起来挺繁琐的,对吧? compose 函数就能帮你简化这个过程,让你像搭积木一样,把这些步骤组合起来。

compose 函数的诞生

compose 函数的核心思想是:将多个函数像管道一样连接起来,一个函数的输出作为下一个函数的输入,最终返回一个组合后的新函数。 执行顺序是从右向左,这就像剥洋葱,一层一层地剥开。

让我们先来一个最基础版本的 compose

function compose(...fns) {
  return function composed(...args) {
    let result = fns.reduceRight((acc, fn) => fn(acc), ...args);
    return result;
  };
}

这个版本的 compose 接收任意数量的函数作为参数,然后返回一个新的函数 composedcomposed 函数接受初始参数,然后使用 reduceRight 从右向左依次执行传入的函数。

  • ...fns: 使用剩余参数语法,收集所有传入的函数到一个数组 fns 中。
  • fns.reduceRight((acc, fn) => fn(acc), ...args): reduceRight 方法从数组的末尾开始遍历,将每个函数依次应用到累积值 acc 上。 初始累积值是传入的参数 ...args

现在,让我们用这个 compose 函数来简化我们的三明治制作过程:

function toastBread(bread) {
  return `烤${bread}`;
}

function spreadButter(bread) {
  return `黄油${bread}`;
}

function addHamAndCheese(bread) {
  return `火腿奶酪${bread}`;
}

function closeSandwich(sandwich) {
  return `盖上面包的${sandwich}`;
}

const makeSandwich = compose(
  closeSandwich,
  addHamAndCheese,
  spreadButter,
  toastBread
);

const finalSandwich = makeSandwich("面包");
console.log(finalSandwich); // "盖上面包的火腿奶酪黄油烤面包"

看到了吗? 代码瞬间简洁了不少。 compose 函数就像一个魔法棒,把这些函数串联起来,形成了一个新的、更强大的函数 makeSandwich

compose 函数的进阶之路

上面的 compose 函数虽然能用,但还不够完美。 它只能处理接收单个参数的函数。 如果我们的函数需要接收多个参数怎么办? 我们需要对 compose 函数进行一些升级。

function compose(...fns) {
  return function composed(...args) {
    let result = args;
    for (let i = fns.length - 1; i >= 0; i--) {
      result = [fns[i](...result)]; // 将结果作为数组传递
    }
    return result[0];
  };
}

这个升级后的 compose 函数,内部的 composed 函数现在将每次函数调用的结果包装在一个数组中,并作为参数传递给下一个函数。最终结果也从数组中提取出来。

再看一个更强大的版本,它可以处理任何数量的参数,并且更加通用:

function compose(...fns) {
  return function composed(...args) {
    if (fns.length === 0) {
      return args.length > 0 ? args[0] : undefined;
    }

    if (fns.length === 1) {
      return fns[0](...args);
    }

    return fns.reduceRight(
      (res, fn) => fn(res),
      fns.pop()(...args) // 先执行最后一个函数,并将结果作为初始值
    );
  };
}

这个版本考虑了以下情况:

  • 如果没有传入任何函数,则直接返回传入的参数(如果有的话)。
  • 如果只传入一个函数,则直接执行该函数并返回结果。
  • 否则,使用 reduceRight 从右向左依次执行函数,并将前一个函数的结果作为下一个函数的参数。 这里 fns.pop()(...args) 先执行最后一个函数,并将结果作为 reduceRight 的初始值。

compose 函数的应用场景

compose 函数在函数式编程中有着广泛的应用,它可以帮助我们:

  • 简化代码: 将多个函数组合成一个更复杂的函数,减少代码的冗余。
  • 提高可读性: 将复杂的逻辑分解成多个小的、易于理解的函数,然后使用 compose 将它们组合起来。
  • 增强可测试性: 由于每个小的函数都是独立的,因此可以更容易地进行单元测试。
  • 实现中间件模式: 在请求处理管道中,可以使用 compose 将多个中间件函数组合起来,形成一个处理链。

举几个实际的例子:

  1. 数据转换:
function toUpperCase(str) {
  return str.toUpperCase();
}

function trim(str) {
  return str.trim();
}

function addExclamation(str) {
  return str + "!";
}

const formatString = compose(addExclamation, toUpperCase, trim);

const result = formatString("  hello world  ");
console.log(result); // "HELLO WORLD!"
  1. 验证表单:
function isNotEmpty(value) {
  return value !== "";
}

function isEmail(value) {
  return /^[^s@]+@[^s@]+.[^s@]+$/.test(value);
}

function isValidPassword(value) {
  return value.length >= 8;
}

const validateEmail = compose(isEmail, isNotEmpty);
const validatePassword = compose(isValidPassword, isNotEmpty);

console.log(validateEmail("[email protected]")); // true
console.log(validatePassword("1234567")); // false
  1. Redux 中的中间件:

Redux 的 applyMiddleware 函数就是使用了 compose 来组合多个中间件,形成一个增强的 dispatch 函数。

compose 函数的注意事项

  • 函数参数的顺序: compose 函数的执行顺序是从右向左,因此要特别注意函数参数的顺序。
  • 函数的类型: compose 函数要求每个函数都接收一个参数,并返回一个值。 如果函数的参数类型不匹配,可能会导致错误。
  • 调试:compose 函数出现问题时,调试起来可能会比较困难。 可以尝试将 compose 函数拆解开来,逐个执行每个函数,以便找到问题所在。

compose 函数 vs pipe 函数

pipe 函数与 compose 函数非常相似,唯一的区别是执行顺序。 compose 函数从右向左执行,而 pipe 函数从左向右执行。

function pipe(...fns) {
  return function piped(...args) {
    return fns.reduce((res, fn) => fn(res), ...args);
  };
}

pipe 函数更符合我们阅读代码的习惯,因此在某些情况下,使用 pipe 函数可能更易于理解。

总结

compose 函数是函数式编程中一个非常强大的工具,它可以帮助我们简化代码、提高可读性、增强可测试性。 虽然 compose 函数的实现可能看起来比较复杂,但只要理解了它的核心思想,就能灵活地运用它来解决各种问题。

为了方便大家理解,我把今天讲的内容整理成一个表格:

特性 描述
核心思想 将多个函数像管道一样连接起来,一个函数的输出作为下一个函数的输入,最终返回一个组合后的新函数。
执行顺序 从右向左(compose)或从左向右(pipe)。
应用场景 数据转换、验证表单、Redux 中间件、请求处理管道等。
优点 简化代码、提高可读性、增强可测试性。
注意事项 函数参数的顺序、函数的类型、调试。
pipe 的区别 compose 从右向左执行,pipe 从左向右执行。

希望今天的讲座能帮助大家更好地理解 compose 函数。 记住,编程就像做菜,掌握了基本的工具和技巧,就能做出美味佳肴! 下次见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注