简单粗暴详细讲解javascript实现函数柯里化与反柯里化

  函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感;下面来一起看看究竟什么是函数柯里化:
  
  维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。其由数学家Haskell Brooks Curry提出,并以curry命名。
  
  概念往往都是干涩且难懂的,让我们用人话来解释就是:如果我们不确定这个函数有多少个参数,我们可以先给它传入一个参数,然后通过JS闭包(如若不懂JS闭包,请先学习闭包知识点再来学习本篇博文  https://www.cnblogs.com/dengyao-blogs/p/11475575.html )来进行返回一个函数,内部函数接收除开第一个参数外的其余参数进行操作并输出,这个就是函数的柯里化;
  
  举个小例子:
  
  场景(需求):
  
  众所周知程序员每天加班的时间还是比较多的,如果我们需要计算一个程序员每天的加班时间,那么我们的第一反应应该是这样;
  
  var overtime=0;
  
  function time(x){
  
  return overtime+=x;

  上面的代码固然没有问题,可是需要每天调用都算加一下当天的时间,很麻烦,并且每调用一次函数都要进行一定的操作,如果数据量巨大,有可能会有影响性能的风险,那么有没有可以偷懒又能解决问题的办法呢?有的!
  
  function time(x){
  
  return function(y){
  
  return x+y;
  
  }
  
  }
  
  var times=time(0);
  
  times(3);
  
  但是上面代码依然存在问题,在实际开发中很多时候我们的参数是不确定的,上面代码虽然简单的实现了柯里化的基本操作,但是对于参数不确定的情况是处理不了的;所以存在着函数参数的局限性;不过我们从上面的代码中基本可以知道函数柯里化是个啥意思了;就是一个函数调用的时候只允许传入一个参数,然后通过闭包返回内部函数去处理和接收剩余参数,返回的函数通过闭包的方式记住了time的第一个参数;
  
  我们再来把代码改造一下:
  
  //  首先定义一个变量接收函数
  
  var overtime = (function() {
  
  //定义一个数组用来接收参数
  
  var args = [];
  
  //这里运用闭包,调用外部函数返回一个内部函数
  
  return function() {
  
  //arguments是浏览器内置对象,专门用来接收参数
  
  //如果参数的长度为0即没有参数的时候
  
  if(arguments.length === 0) {
  
  //定义变量用来累加
  
  var time = 0;
  
  //循环累加,用i和args的长度进行比较
  
  for (var i = 0, l = args.length; i < l; i++) {
  
  //进行累加操作   等价于time=time+args[i]
  
  time += args[i];
  
  }
  
  // 返回累加的结果
  
  return time;
  
  //如果arguments对象参数长度不为零,即有参数的时候
  
  }else {
  
  //定义的空数组添加arguments参数作为数组项,第一个参数古args作为改变this指向,第二个参数arguments把剩余参数作为数组形式添加至空数组中
  
  [].push.apply(args, arguments);
  
  }
  
  }
  
  })();
  
  overtime(3.5);    // 第一天
  
  overtime(4.5);    // 第二天
  
  overtime(2.1);    // 第三天
  
  //...
  
  console.log( overtime() );    // 10.1
  
  代码经过我们的改造已经实现了功能,但是这不是一个函数柯里化的完整实现,那么我们要怎么完整实现呢?下面我们来介绍一种通用的实现方式:
  
  通用的实现方式:
  
  //定义方法currying,先传入一个参数
  
  var currying=function(fn){
  
  //定义空数组装arguments对象的剩余参数
  
  var args=[];
  
  //利用闭包返回一个函数处理剩余参数
  
  return function (){
  
  //如果arguments的参数长度为0,即没有剩余参数
  
  if(arguments.length===0){
  
  //执行上面方法
  
  return fn.apply(this,args)
  
  }
  
  console.log(arguments)
  
  //如果arguments的参数长度不为0,即还有剩余参数
  
  //在数组的原型对象上添加数组,apply用来更改this的指向为args
  
  //将[].slice.call(arguments)的数组添加到原型数组上
  
  //这里的[].slice.call(arguments)===Array.prototype.slice.call(arguments)实质上就是将arguments对象转成数组并具有slice功能
  
  Array.prototype.push.apply(args,[].slice.call(arguments))
  
  //args.push([].slice.call(arguments))
  
  console.log(args)
  
  //这里返回的arguments.callee是返回的闭包函数,callee是arguments对象里面的一个属性,用于返回正被执行的function对象
  
  return arguments.callee
  
  }
  
  }
  
  //这里调用currying方法并传入add函数,结果会返回闭包内部函数
  
  var s=currying(add);
  
  //调用闭包内部函数,当有参数的时候会将参数逐步添加到args数组中,待没有参数传入的时候直接调用
  
  //调用的时候支持链式操作
  
  s(1)(2)(3)();
  
  //也可以一次性传入多个参数
  
  s(1,2,3);
  
  console.log(s());
  
  JS函数柯里化的优点:
  
  1.可以延迟计算,即如果调用柯里化函数传入参数是不调用的,会将参数添加到数组中存储,等到没有参数传入的时候进行调用;
  
  2.参数复用,当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选。
  
  世间万物相对,有因必有果,当然了,有柯里化必然有反柯里化;
  
  反柯里化(uncurrying),从字面意思上来讲就是跟柯里化的意思相反;其实真正的反柯里化的作用是扩大适用范围,就是说当我们调用某个方法的时候,不需要考虑这个对象自身在设计的过程中有没有这个方法,只要这个方法适用于它,我们就可以使用;(这里引用的是动态语言中的鸭子类型的思想)
  
  在学习JS反柯里化之前,我们先学习一下动态语言的鸭子类型思想,以助于我们更好的理解:
  
  动态语言鸭子类型思想(维基百科解释):
  
  在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。
  
  在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。
  
  这个概念的名字来源于由 James Whitcomb Riley 提出的鸭子测试,“鸭子测试”可以这样表述:
  
  当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
  
  理论上的解释往往干涩难懂,换成人话来说就是:你是你妈妈的儿子/女儿,不管你是否优秀,是否漂亮,只要你是你妈亲生的,那么你就是你妈的儿子/女儿;换成鸭子类型就是,只要你会呱呱叫,走起来像鸭子,只要你拥有的行为像鸭子,不管你是不是鸭子,那么你就可以被称为鸭子;
  
  在Javascript中有很多鸭子类型的引用,比如我们在对一个变量进行赋值的时候,显然是不需要考虑对象的类型的,正是因为如此,Javascript才更加的灵活,所以Javascript是一门典型的动态类型语言;
  
  我们来看一下反柯里化中是怎么引用鸭子类型的:
  
  //函数原型对象上添加uncurring方法
  
  Function.prototype.uncurring = function() {
  
  //改变this的指向
  
  //这里的this指向是Array.prototype.push
  
  var self = this;
  
  //这里的闭包用来返回内部函数的执行
  
  return function() {
  
  //创建一个变量,在数组的原型对象上添加shift上面删除第一个参数
  
  //改变数组this的指向为arguments
  
  var obj = Array.prototype.shift.call(arguments);
  
  //最后返回执行并给方法改变指向为obj也就是arguments
  
  // 并传入arguments作为参数
  
  return self.apply(obj, arguments);
  
   buildscript {
  
  repositories {
  
  jcenter(www.shenzhenztgs.com) // this applies only to the Gradle 'Shadow' plugin
  
  plugins {
  
  id 'java'
  
  id 'application'
  
  // shadow plugin to produce fat JARs
  
  id 'com.github.johnrengelman.shadow' version '2.0.4'
  
  javaVersion = '1.8'
  
  flinkVersion = www.hongshengyl.cn'1.9.0'
  
  scalaBinaryVersion = '2.11'
  
  slf4jVersion = '1.7.7'
  
  log4jVersion = '1.2.17'
  
  sourceCompatibility = javaVersion
  
  targetCompatibility = javaVersion
  
  tasks.withType(JavaCompile) {
  
  options.encoding = 'UTF-8'
  
  }
  
  applicationDefaultJvmArgs = [www.qjljdgt.cn"-Dlog4j.configuration=log4j.properties"]
  
  task wrapper(type: Wrapper) {
  
  gradleVersion = '3.1'
  
  }
  
  // declare where to find the dependencies of your project
  
  repositories {
  
  mavenCentral(www.tyyLeapp.com)
  
  maven { url "https://www.jintianxuesha.com .apache.org/content/repositories/snapshots/" }
  
  // 注意:我们不能使用 "compileOnly" 或者 "shadow" 配置,这会使我们无法在 IDE 中或通过使用 "gradle run" 命令运行代码。
  
  // 我们也不能从 shadowJar 中排除传递依赖(请查看 https://www.gaodeyulee.com github.com/johnrengelman/shadow/issues/159)。
  
  // -> 显式定义我们想要包含在 "flinkShadowJar" 配置中的类库!
  
  configurations {
  
  flinkShadowJar // dependencies which go into the shadowJar
  
  // 总是排除这些依赖(也来自传递依赖),因为 Flink 会提供这些依赖。
  
  flinkShadowJar.exclude group: www.hongshengyl.cn'org.apache.flink', module: 'force-shading'
  
  flinkShadowJar.exclude group: www.qiaoheibpt.com 'com.google.code.findbugs', module: 'jsr305'
  
  flinkShadowJar.exclude group: 'org.slf4j'
  
  flinkShadowJar.exclude group: 'log4j'
  
  //数组原型对象上添加uncurrying方法
  
  var push = Array.prototype.push.uncurring();
  
  //测试一下
  
  //匿名函数自执行
  
  (function() {
  
  //这里的push就是一个函数方法了
  
  //相当于传入参数arguments和4两个参数,但是在上面shift方法中删除第一个参数,这里的arguments参数被截取了,所以最后实际上只传入了4
  
  push(arguments, 4);
  
  console.log(arguments); //[1, 2, 3, 4]
  
  //匿名函数自调用并带入参数1,2,3
  
  })(1, 2, 3)
  
  到这里大家可以想一想arguments是一个接收参数的对象,里面是没有push方法的,那么arguments为什么能调用push方法呢?
  
  这是因为代码var push = Array.prototype.push.uncurring();在数组的原型对象的push方法上添加了uncurring方法,然后在执行匿名函数的方法push(arguments, 4);时候实质上是在调用上面的方法在Function的原型对象上添加uncurring方法并返回一个闭包内部函数执行,在执行的过程中因为Array原型对象上的shift方法会把 push(arguments, 4);中的arguments截取,所以其实方法的实际调用是push(4),所以最终的结果才是[1,2,3,4]

你可能感兴趣的:(简单粗暴详细讲解javascript实现函数柯里化与反柯里化)