JS-手写 compose

668 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

前言

compose 翻译成中文是 组成构成的意思,而对于函数而言,compose 函数用于将多个函数组成新的函数,然后按照一定的顺序执行,并且前一个函数的返回值作为下一个函数的参数

对于 JS 而言,并没有提供现成的 compose 函数,需要引入第三方库或者手动实现。

compose 的作用

假设存在以下几个函数:

/** 数字加一 */
function addOne (num) {
  return num + 1
}

/** 数字减去十 */
function subTen (num) {
  return num - 10
}

/** 数字乘以二 */
function double (num) {
  return num * 2
}

/** 数组除以三 */
function divThree (num) {
  return num / 3
}

现在,需要按照一定的逻辑顺序执行以上函数,那么一般写法如下;

console.log('result:', double(subTen(addOne(divThree(9)))))
// result: -12

这种写法主要有两个缺点

  1. 不可重复利用,如果这段逻辑需要执行多次,那么需要多次写出 double(subTen(addOne(divThree(9))));
  2. 嵌套层数过多,不利于阅读和修改;

如果使用了 compose 函数,那么只需要先使用 compose 将指定的函数按照顺序组成新的函数,直接执行该函数便可以得到想要的结果,代码如下:

// 组成新的函数
const compose1 =  compose(divThree, addOne, subTen, double)
// 执行新的函数
console.log('compose1 result:', compose1(9))
// compose1 result: -12

这样的代码易于阅读和修改,并且可以重复利用。

实现 compose

实现 compose 的基本思路也很简单,把传入的函数按照顺序放入数组中,遍历数组,执行数组中的每一项,将执行的返回值保存起来,作为下一个函数的参数,如此循环,直至数组遍历完成。

看上去很简单,但实际写代码有几个小细节需要注意,具体步骤如下:

  1. 定义 compose 函数,传入的参数必须是函数;
  2. 使用 ...fns 接受全部的参数,同时也将参数存入了数组中,方便后续的遍历;
  3. 如果不存在参数 fns,则会返回默认函数;
  4. 检测每个参数是都为函数类型,如果有一个不是,则报错;
  5. 返回新的函数 returnFunction,该函数接受的参数使用 ...arg 存储,同时也是一个闭包,只要该函数可以访问上下文中 fns
    5.1 开始遍历函数数组 fns,取出当前需要执行的函数 fn
    5.2 传入参数,执行函数的数组:
    5.2.1 如果是第一个函数,其参数为 returnFunction 接受到的参数 arg,因为 arg 被转换为数组,所以需要手动拆分一下,代码为 fn(...arg),将其返回值 value 保存起来;
    5.2.2 如果不是第一个函数,取上一个函数的返回值作为参数,执行 fn(value), 将其返回值 value 保存起来;
    5.3 回到 5.1,直到 fns 被遍历完成为止;
    5.4 返回最终的执行结果。

完整实现代码如下:

/**
 * 函数组合
 * @param { function[] } fns 参数必须全是数组
 */
function compose (...fns) {
  // 不存在参数,返回默认函数
  if (fns.length === 0) return num => num
  // 参数检测
  const nonFunction = fns.find(fn => typeof fn !== 'function')
  if (nonFunction) {
    throw new TypeError(`${nonFunction} is not a function !`)
  }
  // 参数符合要求,返回一个组合函数,本质上是按顺序调用传入的函数
  return (...arg) => {
    return fns.reduce((params, fn) => {
      // arg 作为参数被手动转换为数组,如果接受的参数是 arg, 必须拆分传入
      return params === arg ? fn(...params) : fn(params)
    }, arg)
  }
}

其中遍历 fns 的方式有很多,本文使用 reduce 进行遍历,这样可以直接返回最终的值,相比于其他遍历方法,省去了一些代码,可阅读性也好。

对于 fns 的类型检测,也可以放在最后的遍历 fns 中执行;虽然这样看上去少了检测的循环遍历,提高了性能,但是这样只要在执行创建的函数时,才能检测到错误,而且每次执行创建的函数都需要检测 fns,反而提高了执行时间,也没必要多次检测 fns