高级函数技巧-函数柯里化

栏目: JavaScript · 发布时间: 7年前

内容简介:我们经常说在Javascript语言中,如果你对函数式编程有一定了解,柯里化(Currying),维基百科上的解释是,把接受多个参数的函数转换成接受一个单一参数的函数

我们经常说在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, 变量存取慢,访问性很差,

参考链接


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Probabilistic Method Second Edition

The Probabilistic Method Second Edition

Noga Alon、Joel H. Spencer / Wiley-Blackwell / 2000 / $121.95

The leading reference on probabilistic methods in combinatorics-now expanded and updated When it was first published in 1991, The Probabilistic Method became instantly the standard reference on one......一起来看看 《The Probabilistic Method Second Edition》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具