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 相关文章推荐
不错的一个日期输入 动态
Nov 06 Javascript
jquery模拟进度条实现方法
Aug 03 Javascript
javascript实现uploadify上传格式以及个数限制
Nov 23 Javascript
javaScript知识点总结(必看篇)
Jun 10 Javascript
node.js用fs.rename强制重命名或移动文件夹的方法
Dec 27 Javascript
express+mockjs实现模拟后台数据发送功能
Jan 07 Javascript
WebSocket的简单介绍及应用
May 23 Javascript
node实现简单的增删改查接口实例代码
Aug 22 Javascript
js函数柯里化的方法和作用实例分析
Apr 11 Javascript
jQuery实现移动端图片上传预览组件的方法分析
May 01 jQuery
vue-quill-editor的使用及个性化定制操作
Aug 04 Javascript
解决Vue大括号字符换行踩的坑
Nov 09 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
IIS6.0+PHP5.x+MySQL5.x+Zend3.0x+GD+phpMyAdmin2.8x通用安装实例(已经完成)
2006/12/06 PHP
PHP的关于变量和日期处理的一些面试题目整理
2015/08/10 PHP
PHP实现删除字符串中任何字符的函数
2015/08/11 PHP
php模拟实现斗地主发牌
2020/04/22 PHP
jQuery 学习入门篇附实例代码
2010/03/16 Javascript
return false;和e.preventDefault();的区别
2010/07/11 Javascript
IE6弹出“已终止操作”的解决办法
2010/11/27 Javascript
JavaScript ECMA-262-3 深入解析.第三章.this
2011/09/28 Javascript
jquery滚动条插件jScrollPane的使用介绍
2013/11/08 Javascript
Jquery each方法跳出循环,并获取返回值(实例讲解)
2013/12/12 Javascript
jquery 操作css样式、位置、尺寸方法汇总
2014/11/28 Javascript
第一章之初识Bootstrap
2016/04/25 Javascript
防止Node.js中错误导致进程阻塞的办法
2016/08/11 Javascript
jquery 动态合并单元格的实现方法
2016/08/26 Javascript
快速搭建vue2.0+boostrap项目的方法
2018/04/09 Javascript
vue 利用路由守卫判断是否登录的方法
2018/09/29 Javascript
JavaScript Dom 绑定事件操作实例详解
2019/10/02 Javascript
关于IDEA中的.VUE文件报错 Export declarations are not supported by current JavaScript version
2020/10/17 Javascript
python的Crypto模块实现AES加密实例代码
2018/01/22 Python
用Python+OpenCV对比图像质量的几种方法
2019/07/15 Python
python如何实现从视频中提取每秒图片
2020/10/22 Python
Django中的模型类设计及展示示例详解
2020/05/29 Python
python实现粒子群算法
2020/10/15 Python
五种Python转义表示法
2020/11/27 Python
CSS3使用多列制作瀑布流
2016/05/10 HTML / CSS
日本最新流行服饰网购:Nissen
2016/07/24 全球购物
花园仓库建筑:Garden Buildings Direct
2018/02/16 全球购物
RIP版本1跟版本2的区别
2013/12/30 面试题
赞美老师的演讲稿
2014/05/22 职场文书
师德师风建设整改措施思想汇报
2014/10/11 职场文书
后勤个人工作总结
2015/02/28 职场文书
2015年大学生实习评语
2015/03/25 职场文书
教师读书笔记
2015/06/29 职场文书
2019数学教师下学期工作总结
2019/06/27 职场文书
如何解决springcloud feign 首次调用100%失败的问题
2021/06/23 Java/Android
在js中修改html body的样式
2021/11/11 Javascript