javascript实现函数柯里化与反柯里化过程解析


Posted in Javascript onOctober 08, 2019

函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感;下面来一起看看究竟什么是函数柯里化:

维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。其由数学家Haskell Brooks Curry提出,并以curry命名。

概念往往都是干涩且难懂的,让我们用人话来解释就是:如果我们不确定这个函数有多少个参数,我们可以先给它传入一个参数,然后通过JS闭包(如若不懂JS闭包,请先学习闭包知识点再来学习本篇博文https://3water.com/article/171398.htm)来进行返回一个函数,内部函数接收除开第一个参数外的其余参数进行操作并输出,这个就是函数的柯里化;

举个小例子:

场景(需求):

众所周知程序员每天加班的时间还是比较多的,如果我们需要计算一个程序员每天的加班时间,那么我们的第一反应应该是这样;

var overtime=0;
function time(x){
  return overtime+=x;
}

time(1); //1
time(2); //3
time(3); //6

上面的代码固然没有问题,可是需要每天调用都算加一下当天的时间,很麻烦,并且每调用一次函数都要进行一定的操作,如果数据量巨大,有可能会有影响性能的风险,那么有没有可以偷懒又能解决问题的办法呢?有的!

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){


//执行上面方法


//这里的this指向下面的s,类似于s(),代表参数长度为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);
 };
};

//数组原型对象上添加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设计模式与开发实践》一书中,JS函数的反柯里化的案例是这样写的:

//定义一个对象
var obj = {
  "length":1,
  "0":1
}
//在Function原型对象定义方法uncurrying
Function.prototype.uncurrying = function() {
  //this指向Array.prototype.push
  var self = this;
  //闭包返回一个内部函数
  return function() {
  // 这里可以拆开理解
  //首先执行apply return 
  //Function.prototype.call(Array.prototype.push[obj,2])
  //然后Array.prototype.push.call(obj,2)
  //call改变指向 obj.push(2)
  //所以最后结果就是 {0: 1, 1: 2, length: 2}
    return Function.prototype.call.apply(self, arguments);
}
}

//在
var push = Array.prototype.push.uncurrying()

push(obj, 2) 
console.log(obj);
//{0: 1, 1: 2, length: 2}

上面的方式不好理解?没关系,咱们来个好理解的:

Function.prototype.unCurrying = function () {
  var self = this;
  return function () {

//[].slice.call(arguments,1)===Array.prototype.push.slice.call(arguments,1)===arguments.slice(1)
return self.apply(arguments[0], [].slice.call(arguments, 1));
  };
};
var push = Array.prototype.push.uncurrying()
console.log(push);
push(obj,2) //{0: 1, 1: 2, length: 2}
console.log(obj);

分析一下:

1、首先在Function原型对象上添加uncurrying方法,这样所有的Function都可以借用;

2、返回一个闭包内部函数

3、闭包函数返回的结果中返回的是调用方法,self指向Array.prototype.push,apply方法中第一个参数是更改指向,对应下面push(obj,2)相当于更改指向为obj.push(2)

4、apply方法中第二个参数的call方法是更改指向为arguments,并且arguments中能使用slice方法,等于arguments.slice(1)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript实现上传图片前的预览(TX的面试题)
Aug 20 Javascript
jquery 将disabled的元素置为enabled的三种方法
Jul 25 Javascript
javascript iframe编程相关代码
Dec 28 Javascript
经过绑定元素时会多次触发mouseover和mouseout事件
Feb 28 Javascript
JSP中使用JavaScript动态插入删除输入框实现代码
Jun 13 Javascript
Windows8下搭建Node.js开发环境教程
Sep 03 Javascript
jQuery常用知识点总结以及平时封装常用函数
Feb 23 Javascript
Angularjs实现搜索关键字高亮显示效果
Jan 17 Javascript
深入理解vue-router之keep-alive
Aug 31 Javascript
详解react-router 4.0 下服务器如何配合BrowserRouter
Dec 29 Javascript
vue cli 全面解析
Feb 28 Javascript
Node.js折腾记一:读指定文件夹,输出该文件夹的文件树详解
Apr 20 Javascript
新手入门js闭包学习过程解析
Oct 08 #Javascript
关于JSON解析的实现过程解析
Oct 08 #Javascript
解决webpack多页面内存溢出的方法示例
Oct 08 #Javascript
javascript简单实现深浅拷贝过程详解
Oct 08 #Javascript
webpack HappyPack实战详解
Oct 08 #Javascript
简单了解vue中的v-if和v-show的区别
Oct 08 #Javascript
在Koa.js中实现文件上传的接口功能
Oct 08 #Javascript
You might like
Php Cookie的一个使用注意点
2008/11/08 PHP
PHP和Mysqlweb应用开发核心技术 第1部分 Php基础-3 代码组织和重用2
2011/07/03 PHP
php中3种方法删除字符串中间的空格
2014/03/10 PHP
PHP如何实现Unicode和Utf-8编码相互转换
2015/07/29 PHP
PHP如何将XML转成数组
2016/04/04 PHP
PHPExcel简单读取excel文件示例
2016/05/26 PHP
php 多个变量指向同一个引用($b = &amp;$a)用法分析
2019/11/13 PHP
laravel excel 上传文件保存到本地服务器功能
2019/11/14 PHP
事件冒泡是什么如何用jquery阻止事件冒泡
2013/03/20 Javascript
js的touch事件的实际引用
2014/10/13 Javascript
jQuery中append()方法用法实例
2014/12/25 Javascript
jQuery验证元素是否为空的两种常用方法
2015/03/17 Javascript
javascript函数式编程实例分析
2015/04/25 Javascript
Js实现无刷新删除内容
2015/04/29 Javascript
给easyui的datebox控件添加清空按钮的实现方法
2016/11/09 Javascript
Vue实现搜索 和新闻列表功能简单范例
2018/03/16 Javascript
Vue实现美团app的影院推荐选座功能【推荐】
2018/08/29 Javascript
NodeJs实现简单的爬虫功能案例分析
2018/12/05 NodeJs
[02:49]2014DOTA2电竞也是体育项目! 势要把荣誉带回中国!
2014/07/20 DOTA
CentOS 7下Python 2.7升级至Python3.6.1的实战教程
2017/07/06 Python
浅谈Python中重载isinstance继承关系的问题
2018/05/04 Python
python中协程实现TCP连接的实例分析
2018/10/14 Python
Python SMTP发送邮件遇到的一些问题及解决办法
2018/10/24 Python
CentOS 7 安装python3.7.1的方法及注意事项
2018/11/01 Python
基于django channel实现websocket的聊天室的方法示例
2019/04/11 Python
python+selenium定时爬取丁香园的新型冠状病毒数据并制作出类似的地图(部署到云服务器)
2020/02/09 Python
高清屏下canvas重置尺寸引发的问题的解决
2019/10/14 HTML / CSS
银行出纳岗位职责
2013/11/25 职场文书
项目采购员岗位职责
2014/04/15 职场文书
我的小天地教学反思
2014/04/30 职场文书
文明礼仪演讲稿
2014/05/12 职场文书
小学二年级数学教学计划
2015/01/20 职场文书
基层组织建设年活动总结
2015/05/09 职场文书
同意离婚答辩状
2015/05/22 职场文书
小学体育组工作总结2015
2015/07/21 职场文书
解决mysql:ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO/YES)
2021/06/26 MySQL