资讯专栏INFORMATION COLUMN

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

shixinzhang / 2389人阅读

摘要:如果你对函数式编程有一定了解,函数柯里化是不可或缺的,利用函数柯里化,可以在开发中非常优雅的处理复杂逻辑。同样先看简单版本的方法,以方法为例,代码来自高级程序设计加强版实现上面函数,可以换成任何其他函数,经过函数处理,都可以转成柯里化函数。

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

参考链接

JS 函数式编程指南

邂逅函数柯里化

掌握JavaScript函数的柯里化

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102033.html

相关文章

  • JavaScript 高级技巧——“高级函数”的注意要点

    摘要:语法如下注意这里使用的并不是的,是内部函数的。函数柯里化的基本方法是使用一个闭包返回一个函数。当函数被调用时,返回的函数还需要设置一些传入的参数。 安全的类型检测 typeof操作符 检测数据类型的结果可能会不正确; instanceof操作符 操作符在多个全局作用域下存在问题: var value = []; var isArray = value instanceof Array;...

    solocoder 评论0 收藏0
  • JS程序设计高级技巧

    摘要:关于定时器要记住的最重要的事情是指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。多个定时器之间的执行间隔会比预期的小解决办法处理中数组分块,,函数节流,实际进行处理的方法实际执行的代码初始处理调用的方法 一、高级函数 安全类型检测 Object.protitype.toString.call(value) 作用域安全的构造函数 function Pers...

    Codeing_ls 评论0 收藏0
  • JavaScript 函数式编程技巧 - 柯里

    摘要:作为函数式编程语言,带来了很多语言上的有趣特性,比如柯里化和反柯里化。在一些函数式编程语言中,会定义一个特殊的占位变量。个人理解不知道对不对延迟执行柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化和反柯里化。 这里可以对照另外一篇介绍 JS 反柯里化 的文章一起看~ 1. 简介 柯里化(Currying)...

    edgardeng 评论0 收藏0
  • js 闭包的使用技巧

    摘要:闭包的学术定义先来参考下各大权威对闭包的学术定义百科闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 前言 上一章讲解了闭包的底层实现细节,我想大家对闭包的概念应该也有了个大概印象,但是真要用简短的几句话来说清楚,这还真不是件容易的事。这里我们就来总结提炼下闭包的概念,以应付那些非专人士的心血来潮。 闭包的学术...

    dendoink 评论0 收藏0
  • JavaScript 函数式编程技巧 - 反柯里

    摘要:作为函数式编程语言,带来了很多语言上的有趣特性,比如柯里化和反柯里化。而反柯里化,从字面讲,意义和用法跟函数柯里化相比正好相反,扩大适用范围,创建一个应用范围更广的函数。作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化和反柯里化。 可以对照另外一篇介绍 JS 柯里化 的文章一起看~ 1. 简介 柯里化,是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩...

    zhjx922 评论0 收藏0

发表评论

0条评论

shixinzhang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<