函数柯里化

吐槽君 分类:javascript

概念

**柯里化:**可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数,是一个逐步接收参数的过程。

就这么看概念有一点抽象,理解起来肯定是一脸懵逼,我们就是用一个add函数的示例,说明一下

function add(a,b){
 return a + b
}
add(1,2) // 3
 

假如把上面的add函数修改完了柯里化函数,这里我们先用伪代码实现以下

function curryingAdd(){
	// 柯里化实现
}
add(1)(2) // 3
 

经过伪代码的实现,我们可以看出柯里化实际上就是把add函数的两个参数a,b,修改成了先传一个参数a此时返回一个函数传入参数b 计算两次传入参数的计算结果。由此可见柯里化函数是一个逐步接收参数的过程。
**
通过上面的伪代码,我们知道了柯里化的实现过程,接下来就来吧伪代码逐步实现以下

实现过程

// 定义柯里化函数,把普通函数包装成为柯里化函数
function currying(fn) {
  let args = [];
  const inner = function () {
    // 每调用一次开始收集参数
    let _args = [].slice.apply(arguments);
    // 如果有参数继续收集,
    // 如果没有参数则把收集到的参数传入函数调用执行
    if (_args.length > 0) {
      args.push(..._args);
      return inner;
    } else {
      // 由于闭包的原因 args 没有被释放,导致下面多次调用时会保留上次传入的参数
      // 这里手动清空一下
      // 在实际开发中没有必要处理,开发过程中就是要收集参数,每次调用都要复用上次的参数
      const params = [...args]
      args = []
      return fn.apply(null, params);
    }
  };
  // 要返回一个函数
  return inner;
}
// 定义普通函数
function add(){
	const args = [].slice.apply(arguments);
  return args.reduce((acc,cur)=>{return acc+cur},0)
}

// 把 add 函数包装成为柯里化函数
const curryingAdd = currying(add)

console.log(curryingAdd(1,2,3)()); // 6
console.log(curryingAdd(1,2)(3)());// 6
console.log(curryingAdd(1)(2)(3)());// 6
 

根据上面的实现过程可以发现,柯里化函数的原理并不复杂,收集好参数在合适的时机调用即可。但是上面的实现过程,导致我们调用的时候在最后面都要执行一个空参数调用一下,有没有一种方案可以不执行最后一步就可以得到结果呢?答案是有的!!!

根据原型与原型链的知识,我们知道函数的原型上面有两个方法toString 和valueOf。js在获取当前变量值的时候,会根据语境,隐式调用valueOf和toString方法进行获取需要的值,事情变得有趣起来

function currying(fn) {
  let args = [];
  const inner = function () {
    let _args = [].slice.apply(arguments);
    args.push(..._args);
    return inner;
  };
  inner.toString = function () {
    const params = [...args]
    args = []
    return fn.apply(null, params);
  };
  inner.valueOf = function () {
    const params = [...args]
    args = []
    return fn.apply(null, params);
  };
  return inner;
}
 

这种方式只能在浏览器中使用,如果使用node.js 环境依然打印出函数的原型。不过原理已经搞清楚了。

柯里化的作用

搞清楚原理有,来看一下柯里化的作用,为什么要用柯里化。根据上面的实现,可以看到柯里化的实现过程也是闭包的过程,所以柯里化具有闭包的有点。

  1. 参数的复用
  2. 提前确认
  3. 延迟执行

先说1和3,参数复用就不用说了,闭包的特性。延迟执行,例如在js中使用的bind的,实现的机制就是柯里化,返回一个函数,在使用的时候在去调用。
提前确认,体现在代码运行过程中,因为每次都返回一个函数,这样我们在运行的时候就知道执行到那个参数时报错,把一个函数内的错误分散到多个函数。

应用场景

来看一下柯里化的一个应用场景,这也是我在实际开发的过程中遇到的一个问题。
题目**:**传入两个日期,计算两个日期相差多少年,多少月,多少天,如果不传入年则返回多少月多少天。

示例:

function diffDate(date1,data2,type){
	// TODO
}
diff('2020-04-01','2021-05-03','year') // 1年1月2天
diff('2020-04-01','2021-05-03','month') // 13月2天
diff('2020-04-01','2021-05-03','day') // 397天
 

解题过程:

在前端项目通常使用moment包处理日期相关内容,目前moment已经不维护,使用day.js 是一样的,这里还是用moment处理。这个题目中是要到的api是diff

const moment = require('moment')

const diffDate = (begin, end) => {
  // 是否过期,判断两个日期大小,如果begin > end 返回 负
  let flag = false;
  const contrast = () => {
    if (moment(begin).isAfter(end)) {
      flag = true;
      return [end, begin];
    }
    return [begin, end];
  };
  // 对比日期,小日期在前面,大日期在后面
  let [from, to] = contrast();
  function computed() {
    const args = [].slice.apply(arguments);
    let diffObj = {};
    args.forEach((arg) => {
      // 根据传入参数。计算时间差
      const diff = Math.abs(moment(from).diff(moment(to), arg));
      // 更新 from 时间,把开始时间更新到对比之后继续对比
      from = moment(from).add(diff, arg);
      diffObj = {
        ...diffObj,
        [arg]: diff,
      };
    });
    return diffObj;
  }
  const computedDate = currying(computed);
  const result = computedDate('Years')('Months')('Days')();
  console.log(flag, result); // false { Years: 1, Months: 1, Days: 2 }
};
diffDate('2021-04-01', '2022-05-03');
 

**分析:**应用场景是计算一个产品开通是否过期,过期多少天,还有多少天到期,到期日期是知道的例如产品A到期日期是2022-05-03,今天是2021-04-01。根据程序要把小日期放到前面,先计算相差多少年(1年),把开始日期from向后更新新一年为2022-04-01,在计算多少月(1月),继续更新。。。直到计算到天。

可以看到使用到的柯里化函数式第一种方式。

回复

我来回复
  • 暂无回复内容