博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
高级函数技巧-函数柯里化
阅读量:6230 次
发布时间:2019-06-21

本文共 4805 字,大约阅读时间需要 16 分钟。

我们经常说在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))    // 8alert(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, 绑定指定环境

实际开发,推荐使用 , 具体实现,可以参考下

使用场景

讲了这么多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 等实现的基础

    有人说柯里化是应函数式编程而生,它在里面出现的概率就非常大了,在中,开篇就介绍了柯里化的重要性。

关于额外开销

函数柯里化可以用来构建复杂的算法 和 功能, 但是滥用也会带来额外的开销。

从上面实现部分的代码中,可以看到,使用柯里化函数,离不开闭包, arguments, 递归。

闭包,函数中的变量都保存在内存中,内存消耗大,有可能导致内存泄漏。

递归,效率非常差,
arguments, 变量存取慢,访问性很差,

参考链接

转载地址:http://jnxna.baihongyu.com/

你可能感兴趣的文章
二分匹配 飞行员配对方案问题
查看>>
几个著名java开源缓存框架的介绍
查看>>
CSS核心内容:层叠和继承
查看>>
windwos phone 的listbox 的各种细节
查看>>
Resin
查看>>
理解Android虚拟机体系结构(转)
查看>>
Redis系列之(一):10分钟玩转Redis(转)
查看>>
调试逆向分为动态分析技术和静态分析技术(转)
查看>>
上传文件的大小限制 专题
查看>>
基于KMP与Levenshtein模糊匹配算法的银行联行号查询(转)
查看>>
BZOJ1303 中位数图
查看>>
生产者消费者模型
查看>>
Multi-level Contextual 3D Convolutional Neural Networks
查看>>
apidoc官网和github地址
查看>>
来到外企几天的感受
查看>>
<a>之间怎么放值</a> 挺简单的,第一次遇到···
查看>>
业界领袖们曾希望在毕业时得到的建议
查看>>
Linux禁用IPv6
查看>>
【转】linux 用户线程、LWP、内核线程学习笔记
查看>>
各种书籍
查看>>