JS中精巧的自动柯里化实现方法


Posted in Javascript onDecember 12, 2017

以下内容通过代码讲解和实例分析了JS中精巧的自动柯里化实现方法,并分析了柯里化函数的基础用法和知识,学习一下吧。

什么是柯里化?

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

理论看着头大?没关系,先看看代码:

柯里化应用

假设我们需要实现一个对列表元素进行某种处理的功能,比如说让列表内每一个元素加一,那么很容易想到:

const list = [0, 1, 2, 3];
list.map(elem => elem + 1);

很简单是吧?如果又要加2呢?

const list = [0, 1, 2, 3];
list.map(elem => elem + 1);
list.map(elem => elem + 2);

看上去效率有点低,处理函数封装下?

可是map的回调函数只接受当前元素 elem 这一个参数,看上去好像没有办法封装...

你也许会想:如果能拿到一个部分配置好的函数就好了,比如说:

// plus返回部分配置好的函数
const plus1 = plus(1);
const plus2 = plus(2);
plus1(5); // => 6
plus2(7); // => 9

把这样的函数传进map:

const list = [0, 1, 2, 3];
list.map(plus1); // => [1, 2, 3, 4]
list.map(plus2); // => [2, 3, 4, 5]

是不是很棒棒?这样一来不管是加多少,只需要list.map(plus(x))就好了,完美实现了封装,可读性大大提高!

不过问题来了:这样的plus函数要怎么实现呢?

这时候柯里化就能派上用场了:

柯里化函数

// 原始的加法函数
function origPlus(a, b) {
 return a + b;
}
// 柯里化后的plus函数
function plus(a) {
 return function(b) {
  return a + b;
 }
}
// ES6写法
const plus = a => b => a + b;

可以看到,柯里化的 plus 函数首先接受一个参数 a,然后返回一个接受一个参数 b 的函数,由于闭包的原因,返回的函数可以访问到父函数的参数 a,所以举个例子:const plus2 = plus(2)就可等效视为function plus2(b) { return 2 + b; },这样就实现了部分配置。

通俗地讲,柯里化就是一个部分配置多参数函数的过程,每一步都返回一个接受单个参数的部分配置好的函数。一些极端的情况可能需要分很多次来部分配置一个函数,比如说多次相加:

multiPlus(1)(2)(3); // => 6

这种写法看着很奇怪吧?不过如果入了JS的函数式编程这个大坑的话,这会是常态。

JS中自动柯里化的精巧实现

柯里化(Currying)是函数式编程中很重要的一环,很多函数式语言(eg. Haskell)都会默认将函数自动柯里化。然而JS并不会这样,因此我们需要自己来实现自动柯里化的函数。

先上代码:

// ES5
function curry(fn) {
 function _c(restNum, argsList) {
  return restNum === 0 ?
   fn.apply(null, argsList) :
   function(x) {
    return _c(restNum - 1, argsList.concat(x));
   };
 }
 return _c(fn.length, []);
}
// ES6
const curry = fn => {
 const _c = (restNum, argsList) => restNum === 0 ?
  fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);
 return _c(fn.length, []);
}
/***************** 使用 *********************/
var plus = curry(function(a, b) {
 return a + b;
});
// ES6
const plus = curry((a, b) => a + b);
plus(2)(4); // => 6

这样就实现了自动的柯里化!

如果你看得懂发生了什么的话,那么恭喜你!大家口中的大佬就是你!,快留下赞然后去开始你的函数式生涯吧(滑稽

如果你没看懂发生了什么,别担心,我现在开始帮你理一下思路。

需求分析

我们需要一个 curry 函数,它接受一个待柯里化的函数为参数,返回一个用于接收一个参数的函数,接收到的参数放到一个列表中,当参数数量足够时,执行原函数并返回结果。

实现方式

简单思考可以知道,柯里化部分配置函数的步骤数等于 fn 的参数个数,也就是说有两个参数的 plus 函数需要分两步来部分配置。函数的参数个数可以通过fn.length获取。

总的想法就是每传一次参,就把该参数放入一个参数列表 argsList 中,如果已经没有要传的参数了,那么就调用fn.apply(null, argsList)将原函数执行。要实现这点,我们就需要一个内部的判断函数 _c(restNum, argsList),函数接受两个参数,一个是剩余参数个数 restNum,另一个是已获取的参数的列表 argsList;_c 的功能就是判断是否还有未传入的参数,当 restNum 为零时,就是时候通过fn.apply(null, argsList)执行原函数并返回结果了。如果还有参数需要传递的话,也就是说 restNum 不为零时,就需要返回一个单参数函数

function(x) {
 return _c(restNum - 1, argsList.concat(x));
}

来继续接收参数。这里形成了一个尾递归,函数接受了一个参数后,剩余需要参数数量 restNum 减一,并将新参数 x 加入 argsList 后传入 _c 进行递归调用。结果就是,当参数数量不足时,返回负责接收新参数的单参数函数,当参数够了时,就调用原函数并返回。

现在再来看:

function curry(fn) {
 function _c(restNum, argsList) {
  return restNum === 0 ?
   fn.apply(null, argsList) :
   function(x) {
    return _c(restNum - 1, argsList.concat(x));
   };
 }
 return _c(fn.length, []); // 递归开始
}

是不是开始清晰起来了?

ES6写法的由于使用了 数组解构 及 箭头函数 等语法糖,看上去精简很多,不过思想都是一样的啦~

// ES6
const curry = fn => {
 const _c = (restNum, argsList) => restNum === 0 ?
  fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);

 return _c(fn.length, []);
}

与其他方法的对比

还有一种大家常用的方法:

function curry(fn) {
 const len = fn.length;
 return function judge(...args1) {
  return args1.length >= len ?
  fn(...args1):
  function(...args2) {
   return judge(...[...args1, ...args2]);
  }
 }
}
// 使用箭头函数
const curry = fn => {
 const len = fn.length;
 const judge = (...args1) => args1.length >= len ?
  fn(...args1) : (...args2) => judge(...[...args1, ...args2]);
 return judge;
}

与本篇文章先前提到的方法对比的话,发现这种方法有两个问题:

依赖ES6的解构(函数参数中的 ...args1 与 ...args2);

性能稍差一点。

性能问题

做个测试:

console.time("curry");
const plus = curry((a, b, c, d, e) => a + b + c + d + e);
plus(1)(2)(3)(4)(5);
console.timeEnd("curry");

在我的电脑(Manjaro Linux,Intel Xeon E5 2665,32GB DDR3 四通道1333Mhz,Node.js 9.2.0)上:

本篇提到的方法耗时约 0.325ms

其他方法的耗时约 0.345ms

差的这一点猜测是闭包的原因。由于闭包的访问比较耗性能,而这种方式形成了两个闭包:fn 和 len,前面提到的方法只形成了 fn 一个闭包,所以造成了这一微小的差距。

Javascript 相关文章推荐
经典的解除许多网站无法复制文字的绝招
Dec 31 Javascript
两个DIV等高的JS的实现代码
Dec 23 Javascript
javascript表单验证 - Parsley.js使用和配置
Jan 25 Javascript
Extjs Gird 支持中文拼音排序实现代码
Apr 15 Javascript
通过js获取div的background-image属性
Oct 15 Javascript
简单时间提示DEMO从0开始一直进行计时
Nov 19 Javascript
jquery获取URL中参数解决中文乱码问题的两种方法
Dec 18 Javascript
jQuery实现简单的点赞效果
May 29 Javascript
JS表单验证的代码(常用)
Apr 08 Javascript
js获取json中key所对应的value值的简单方法
Jun 17 Javascript
微信小程序 slider的简单实例
Apr 19 Javascript
vue el-table实现行内编辑功能
Dec 11 Javascript
Vue2.0 slot分发内容与props验证的方法
Dec 12 #Javascript
分析JS中this引发的bug
Dec 12 #Javascript
微信小程序使用progress组件实现显示进度功能【附源码下载】
Dec 12 #Javascript
基于input动态模糊查询的实现方法
Dec 12 #Javascript
详解vue.js之props传递参数
Dec 12 #Javascript
react实现菜单权限控制的方法
Dec 11 #Javascript
Angular 作用域scope的具体使用
Dec 11 #Javascript
You might like
php smarty函数扩展
2010/03/15 PHP
浅谈php中mysql与mysqli的区别分析
2013/06/10 PHP
PHP获取数组的键与值方法小结
2015/06/13 PHP
几行代码轻松实现PHP文件打包下载zip
2017/03/01 PHP
Laravel学习教程之从入口到输出过程详解
2017/08/27 PHP
ThinkPHP框架下微信支付功能总结踩坑笔记
2019/04/10 PHP
通过PHP的Wrapper无缝迁移原有项目到新服务的实现方法
2020/04/02 PHP
JavaScript创建命名空间(namespace)的最简实现
2007/12/11 Javascript
asp.net+js 实现无刷新上传解析csv文件的代码
2010/05/17 Javascript
jquery blockUI 遮罩不能消失与不能提交的解决方法
2011/09/17 Javascript
S2SH整合JQuery+Ajax实现登录验证功能实现代码
2013/01/30 Javascript
解析javascript 数组以及json元素的添加删除
2013/06/26 Javascript
JavaScript实现的圆形浮动标签云效果实例
2015/08/06 Javascript
微信公众号支付H5调用支付解析
2016/11/04 Javascript
NodeJS中的MongoDB快速入门详细教程
2016/11/11 NodeJs
Angular 1.x个人使用的经验小结
2017/07/19 Javascript
jQuery实现火车票买票城市选择切换功能
2017/09/15 jQuery
浅谈js的解析顺序 作用域 严格模式
2017/10/23 Javascript
vuex提交state&&实时监听state数据的改变方法
2018/09/16 Javascript
利用Electron简单撸一个Markdown编辑器的方法
2019/06/10 Javascript
8个有意思的JavaScript面试题
2019/07/30 Javascript
easy_install python包安装管理工具介绍
2013/02/10 Python
对python借助百度云API对评论进行观点抽取的方法详解
2019/02/21 Python
python对矩阵进行转置的2种处理方法
2019/07/17 Python
python3图片文件批量重命名处理
2019/10/31 Python
python计算波峰波谷值的方法(极值点)
2020/02/18 Python
Python基于traceback模块获取异常信息
2020/07/23 Python
详解scrapy内置中间件的顺序
2020/09/28 Python
使用Python快速打开一个百万行级别的超大Excel文件的方法
2021/03/02 Python
Expedia意大利旅游网站:酒店、机票和租车预订
2017/10/30 全球购物
幼儿园老师辞职信
2014/01/20 职场文书
名企HR怎样看待求职信
2014/02/23 职场文书
红色经典观后感
2015/06/18 职场文书
公司借款担保书
2015/09/22 职场文书
2019升学宴主持词范本5篇
2019/10/09 职场文书
mysql查找连续出现n次以上的数字
2022/05/11 MySQL