函数柯里化

基础概念

当一个函数有多个参数的时候,先传递一部分参数调用他(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,返回结果;
简言之就是:多变量函数拆解为单变量的多个函数的依次调用;


可以干嘛呢?

可以利用它来实现对函数参数的缓存,降低函数粒度,把多元函数转换成一元函数,实现函数的组合,产生更强大的功能


核心流程分析

就是利用闭包和递归调用,可以形成一个不销毁的私有作用域,把预先处理的内容放到不销毁的作用域里面,返回一个函数供以后调用;
举个例子:

比如我们有一个判断用户年龄是否大于某个值的函数

// 普通的纯函数
function checkAge (min, age) {
    return age >= min
}
// 普通调用
console.log(checkAge(18, 20))  //true
console.log(checkAge(18, 24))  //true
console.log(checkAge(60, 30))  //false

可能需要经常判断用户是否成年(大于18岁),为了减少代码重复,所以改造如下

// 柯里化后的函数
function checkAge (min) {
    return function (age) {
        return age >= min
    }
}
const checkAge18 = checkAge(18)
const checkAge60 = checkAge(60)
console.log(checkAge18(20)) //true
console.log(checkAge18(24)) //true
console.log(checkAge60(30)) //false

以上就是一个针对checkAge函数的柯里化改造,他的自由度很低,因此需要封装一个通用的柯里化函数;


实现思路

首先,我们通过调用lodash提供的柯里化函数(curry)来了解一下如何使用,并且分析一下实现思路

const _ = require('lodash')
function getSum (a, b, c) {
  return a + b + c
}
// 定义一个柯里化函数
const curried = _.curry(getSum)

// 如果输入了全部的参数,则立即返回结果
console.log(curried(1, 2, 3)) // 6
//如果传入了部分的参数,此时它会返回当前函数,并且等待接收getSum中的剩余参数
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6

通过以上可以看出,柯里化函数的运行过程其实是一个参数的收集过程,将每一次传入的参数收集起来,在最后统一处理

所以,实现思路:

  • 调用curry,传递一个函数,然后需要返回一个柯里化函数(curried)
  • 如果调用curried传递的参数和getSum参数个数相同,就立即执行并返回结果;如果调用curried传递的是部分参数,那么需要返回一个新函数,等待接受getSum其他参数

具体实现如下:

function curry(func) {
  return function curriedFn(...args) {
    // 若实参的个数小于形参的个数
    if (args.length < func.length) {
      return function () {
        // 等待传递的剩余参数
        // 第一部分参数在args里面,第二部分参数在arguments里面
        return curriedFn(...args.concat(...arguments));
      };
    }
    // 如果实参大于等于形参的个数,立即执行并返回结果
    // args是剩余参数
    return func(...args);
  };
}

注意:这里有个细节,就是要柯理化的函数不能有默认值,否则该函数的length属性将失真;
将造成结果提前返回或者报错

如下:

    • image

该技术的优缺点

上面费那么大劲封装,到底有什么好处呢?

优点:

  • 参数复用;参考上面的checkAge函数,把18这个参数缓存起来,多个地方用到18的就可以直接调用

  • 将多元函数比变成一元函数,然后组合函数产生更强大功能

  • 延迟运行;像经常使用的bind,就是基于柯里化实现的;

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)

    return function() {
        return _this.apply(context, args)
    }
}

那缺点也显然易见:

  • 使用了大量的闭包,内存得不到释放,容易造成内存泄漏

对比传统的函数调用,则不会产生闭包,使用完即可释放

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,只要注意闭包的内存释放即可放心使用。


面试题

一)

// 实现一个add方法,使计算结果能够满足如下预期:
add(1,2,3) = 6;
add(1,2)(3) = 6;
add(1)(2)(3) = 6;

这个题目是想让add函数执行后,返回一个能够继续执行的函数,最终计算出所有参数的和,重点在于每次接受的参数可以有一个,也可以有多个(add接受的参数个数固定);

答案如下:

function curry(func) {
  return function curriedFn(...args) {
    // 若实参的个数小于形参的个数
    if (args.length < func.length) {
      return function () {
        // 等待传递的剩余参数
        // 第一部分参数在args里面,第二部分参数在arguments里面
        return curriedFn(...args.concat(...arguments));
      };
    }
    // 如果实参大于等于形参的个数,立即执行并返回结果
    // args是剩余参数
    return func(...args);
  };
}
function add(a,b,c){
   return a+b+c;
}
const newFn =  curry(add)
newFn(1)(2)(3)  //6
newFn(1,2)(3)   //6
newFn(1,2,3)    //6

上述考题是参数固定:也就是add已知参数就是3个;那参数不固定的,如何解决呢?请看第2题

二)

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

这个题目相较于第1题,它的难点在于add的参数不固定;所以要继续优化;

先来看下面两种解法

解法1.

// 柯里化写法
function sum(...arr) {
  return arr.reduce((per, next) => {
    return per + next;
  }, 0);
}

function curry(fn) {
  let args = [];
  return function curried(...res) {
    if (res.length) {
      args = [...args, ...res];
      return curried;
    } else {
      return fn.apply(this, args);
    }
  };
}
let add = curry(sum);
console.log(add(1)(2)(3)()); //6

解法2.

//toString 写法
function curry(a) {
  function curried(item) {
    a += item;
    return curried;
  }
  curried.toString = function () {
    return a;
  };

  return curried;
}
console.log(curry(1)(2)(3).toString()); //6

以上两种方式虽然都能实现,但是解法1需要最后再调用一次,而解法2需要多调用一个转换函数;
都有点勉强,不太符合考题调用方式;

那来看最后一种实现方式:

解法3.

function add(...args) {
  let final = [...args];
  setTimeout(() => {
    console.log(final.reduce((sum, cur) => sum + cur));
  }, 0);
  const inner = function (...args) {
    final = [...final, ...args];
    return inner;
  };
  return inner;
}
console.log(add(1)(2)(3)); //6

这个方法利用了异步编程,setTimeout中的内容延迟执行,算是个奇淫技巧,但终归是符合了考题的调用方法;

具体使用哪种,还要看面试官想考什么?
如果是考柯里化知识点,那就选解法1
如果必须按照题目方式调用,那只能选择解法3

你可能感兴趣的:(函数柯里化)