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 相关文章推荐
调用js时ie6和ie7,ff的区别
Aug 19 Javascript
Jquery 插件学习实例1 插件制作说明与tableUI优化
Apr 02 Javascript
20款超赞的jQuery插件 Web开发人员必备
Feb 26 Javascript
关于锚点跳转及jQuery下相关操作与插件
Oct 01 Javascript
javascript记录文本框内文字个数检测文字个数变化
Oct 14 Javascript
JQ技术实现注册页面带有校验密码强度
Jul 27 Javascript
Jquery使用小技巧汇总
Dec 29 Javascript
SWFUpload多文件上传及文件个数限制的方法
May 31 Javascript
简单实现轮播图效果的实例
Jul 15 Javascript
JavaScript中的splice方法用法详解
Jul 20 Javascript
微信小程序调用后台service教程详解
Nov 06 Javascript
Vue实现省市区三级联动
Dec 27 Vue.js
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
一个简洁的多级别论坛
2006/10/09 PHP
提高PHP编程效率的53个要点(经验小结)
2010/09/04 PHP
PHP 中关于ord($str)>0x80的详细说明
2012/09/23 PHP
解析PHP多种序列化与反序列化的方法
2013/06/06 PHP
Zend Framework开发入门经典教程
2016/03/23 PHP
PHP使用文件锁解决高并发问题示例
2018/03/29 PHP
PHP使用XMLWriter读写xml文件操作详解
2018/07/31 PHP
php中yii框架实例用法
2020/12/22 PHP
javascript:void(0)的作用示例介绍
2013/10/28 Javascript
基于jQuery的图片不完全按比例自动缩小
2014/07/11 Javascript
Javascript核心读书有感之类型、值和变量
2015/02/11 Javascript
JS和css实现检测移动设备方向的变化并判断横竖屏幕
2015/05/25 Javascript
jQuery实现点击按钮弹出可关闭层的浮动层插件
2015/09/19 Javascript
推荐VSCode 上特别好用的 Vue 插件之vetur
2017/09/14 Javascript
微信小程序wx.navigateTo中events属性实现页面间通信传值,数据同步
2019/07/13 Javascript
layui table表格数据的新增,修改,删除,查询,双击获取行数据方式
2019/11/14 Javascript
如何优雅地取消 JavaScript 异步任务
2020/03/22 Javascript
vue-router 2.0 跳转之router.push()用法说明
2020/08/12 Javascript
nestjs中异常过滤器Exceptionfilter的具体使用
2021/02/07 Javascript
python使用fileinput模块实现逐行读取文件的方法
2015/04/29 Python
在Python中使用sort()方法进行排序的简单教程
2015/05/21 Python
理解生产者消费者模型及在Python编程中的运用实例
2016/06/26 Python
Python自然语言处理之词干,词形与最大匹配算法代码详解
2017/11/16 Python
python的命名规则知识点总结
2019/10/04 Python
python 通过pip freeze、dowload打离线包及自动安装的过程详解(适用于保密的离线环境
2020/12/14 Python
python中K-means算法基础知识点
2021/01/25 Python
Manuka Doctor英国官网:真正的麦卢卡蜂蜜和护肤品
2018/10/26 全球购物
汽车队司机先进事迹材料
2014/02/01 职场文书
歌颂祖国的演讲稿
2014/05/04 职场文书
竞赛口号大全
2014/06/16 职场文书
班级学雷锋活动总结
2014/06/26 职场文书
在职员工证明书
2014/09/19 职场文书
四则混合运算教学反思
2016/02/23 职场文书
Go标准容器之Ring的使用说明
2021/05/05 Golang
为什么mysql字段要使用NOT NULL
2021/05/13 MySQL
在pycharm中无法import所安装的库解决方案
2021/05/31 Python