高等函数技能-函数柯里化

我们经常说在Javascript语言中,函数是“一等国民”,它们本质上是异常简朴和历程化的。能够应用函数,举行一些简朴的数据处置惩罚,return 效果,或许有一些分外的功用,须要经由过程运用闭包来完成,末了经常会return 匿名函数。

假如你对函数式编程有肯定相识,函数柯里化(function currying)是不可或缺的,应用函数柯里化,能够在开辟中异常文雅的处置惩罚庞杂逻辑。

函数柯里化

柯里化(Currying),维基百科上的诠释是,把接收多个参数的函数转换成接收一个单一参数的函数
先看一个简朴例子

    // 柯里化
    var foo = function(x) {
        return function(y) {
            return x + y
        }
    }
    
    foo(3)(4)       // 7

    
    // 一般要领
    var add = function(x, y) {
        return x + y;
    }
    
    add(3, 4)       //7 

原本应当一次传入两个参数的add函数,柯里化要领,变成每次挪用都只用传入一个参数,挪用两次后,取得末了的效果。

再看看,一道典范的面试题。

编写一个sum函数,完成以下功用:
 console.log(sum(1)(2)(3)) // 6.

直接套用上面柯里化函数,多加一层return

   function sum(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }

固然,柯里化不是为相识决面试题,它是应函数式编程而生,

怎样完成

照样看看上面的典范面试题。
假如想完成 sum(1)(2)(3)(4)(5)...(n)就得嵌套n-1个匿名函数,

   function sum(a) {
        return function(b) {
             ...
            return function(n) {
                
            }
        }
    }
    

看起来并不文雅,假如我们预先晓得有多少个参数要传入,能够应用递归要领处理

    var add = function(num1, num2) {
        return num1 + num2;
    }
    
    // 假定 sum 函数挪用时,传入参数都是规范的数字
    function curry(add, n) {
       var count = 0,
           arr = [];
           
       return function reply(arg) {
           arr.push(arg);
           
           if ( ++count >= n) {
               //这里也能够在表面定义变量,保留每次盘算后效果
               return arr.reduce(function(p, c) {
                   return p = add(p, c);
               }, 0) 
           } else {
               return reply;
           }
       }
    }
    var sum = curry(add, 4);
    
    sum(4)(3)(2)(1)  // 10

假如挪用次数多于商定数目,sum 就会报错,我们就能够设想成相似如许

sum(1)(2)(3)(4)(); // 末了传入空参数,标识挪用完毕,

只须要简朴修改下curry 函数

function curry(add) {
       var arr = [];
       
       return function reply() {
         var arg = Array.prototype.slice.call(arguments);
         arr = arr.concat(arg);
         
          if (arg.length === 0) { // 递归完毕前提,修改成 传入空参数
              return arr.reduce(function(p, c) {
                  return p = add(p, c);
              }, 0)
          } else {
              return reply;
          }
      }
    }
  
  console.log(sum(4)(3)(2)(1)(5)())   // 15

简约版完成

上面针对详细问题,引入柯里化要领解答,回到怎样完成建立柯里化函数的通用要领。
一样先看简朴版本的要领,以add要领为例,代码来自《JavaScript高等程序设想》

 function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

function add(num1, num2) {
    return num1 + num2;
}
var curriedAdd = curry(add, 5);

var curriedAdd2 = curry(add, 5, 12);

alert(curriedAdd(3))    // 8
alert(curriedAdd2())    // 17

加强版完成

上面add函数,能够换成任何其他函数,经由curry函数处置惩罚,都能够转成柯里化函数。
这里在挪用curry初始化时,就传入了一个参数,而且返回的函数 curriedAdd , curriedAdd2也没有被柯里化。要想完成越发通用的要领,在柯里化函数真正挪用时,再传参数,

function curry(fn) {
     ...
 }

function add(num1, num2) {
    return num1 + num2;
}

var curriedAdd = curry(add);

curriedAdd(3)(4) // 7

每次挪用curry返回的函数,也被柯里化,能够继承传入一个或多个参数举行挪用,

跟上面sum(1)(2)(3)(4) 异常相似,应用递归就能够完成。 关键是递归的出口,这里不能是传入一个空参数的挪用, 而是原函数定义时,参数的总个数,柯里化函数挪用时,满足了原函数的总个数,就返回盘算效果,不然,继承返回柯里化函数

原函数的入参总个数,能够应用length 属性取得

function add(num1, num2) {
    return num1 + num2;
}

add.length // 2

连系上面的代码,

    var curry = function(f) {
      var len = f.length;
      
        return function t() {
          var innerLength = arguments.length,
            args = Array.prototype.slice.call(arguments);
            
          if (innerLength >= len) {   // 递归出口,f.length
             return f.apply(undefined, args)
          } else {
            return function() {
              var innerArgs = Array.prototype.slice.call(arguments),
                allArgs = args.concat(innerArgs);
                
              return t.apply(undefined, allArgs)
            }
          }
        }
    }
    
   // 测试一下
  function add(num1, num2) {
    return num1 + num2;
  }

   var curriedAdd = curry(add);
   add(2)(3);     //5

  // 一个参数
  function identity(value) {
     return value;
 }

   var curriedIdentify = curry(identify);
   curriedIdentify(4) // 4

到此,柯里化通用函数能够满足大部份需求了。

在运用 apply 递归挪用的时刻,默许传入 undefined, 在别的场景下,能够须要传入 context, 绑定指定环境

现实开辟,引荐运用 lodash.curry , 详细完成,能够参考下curry源码

运用场景

讲了这么多curry函数的差别完成要领,那末完成了通用要领后,在那些场景下能够运用,或许说运用柯里化函数是不是能够实在的进步代码质量,下面总结一下运用场景

  • 参数复用
    在《JavaScript高等程序设想》中简朴版的curry函数中

      var curriedAdd = curry(add, 5)

    在后面,运用curriedAdd函数时,默许都复用了5,不须要从新传入两个参数

  • 耽误实行
    上面传入多个参数的sum(1)(2)(3),就是耽误实行的末了例子,传入参数个数没有满足原函数入参个数,都不会马上返回效果。

    相似的场景,另有绑定事宜回调,更运用bind()要领绑定上下文,传入参数相似,

       addEventListener('click', hander.bind(this, arg1,arg2...))
       
       addEventListener('click', curry(hander)) 
       

    耽误实行的特征,能够防止在实行函数表面,包裹一层匿名函数,curry函数作为回调函数就有很大上风。

  • 函数式编程中,作为compose, functor, monad 等完成的基本

    有人说柯里化是应函数式编程而生,它在内里涌现的几率就异常大了,在JS 函数式编程指南中,开篇就引见了柯里化的重要性。

关于分外开支

函数柯里化能够用来构建庞杂的算法 和 功用, 然则滥用也会带来分外的开支。

从上面完成部份的代码中,能够看到,运用柯里化函数,离不开闭包, arguments, 递归。

闭包,函数中的变量都保留在内存中,内存斲丧大,有能够致使内存走漏。
递归,效力异常差,
arguments, 变量存取慢,接见性很差,

参考链接

    原文作者:不可能的是
    原文地址: https://segmentfault.com/a/1190000018265172
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞