美文网首页
函数柯里化

函数柯里化

作者: Young_Jeff | 来源:发表于2021-09-01 14:22 被阅读0次

    基础概念

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


    可以干嘛呢?

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


    核心流程分析

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

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

    // 普通的纯函数
    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</pre>
    

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

    所以,实现思路:

    • 调用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

    相关文章

      网友评论

          本文标题:函数柯里化

          本文链接:https://www.haomeiwen.com/subject/ewmxwltx.html