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 相关文章推荐
jquery异步调用页面后台方法‏(asp.net)
Mar 01 Javascript
jQuery事件绑定.on()简要概述及应用
Feb 07 Javascript
js判断客户端是iOS还是Android等移动终端的方法
Dec 11 Javascript
jQuery实现图片上传和裁剪插件Croppie
Nov 29 Javascript
学习使用bootstrap3栅格系统
Apr 12 Javascript
Javascript 创建类并动态添加属性及方法的简单实现
Oct 20 Javascript
jQuery使用方法
Feb 04 Javascript
H5图片压缩与上传实例
Apr 21 Javascript
webpack配置导致字体图标无法显示的解决方法
Mar 06 Javascript
通过vue提供的keep-alive减少对服务器的请求次数
Apr 01 Javascript
使用javascript做在线算法编程
May 25 Javascript
解决vue-router 二级导航默认选中某一选项的问题
Nov 01 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 中的面向对象编程:通向大型 PHP 工程的办法
2006/12/03 PHP
PHP中的reflection反射机制测试例子
2014/08/05 PHP
PHP代码优化技巧小结
2015/09/29 PHP
聊聊 PHP 8 新特性 Attributes
2020/08/19 PHP
PHP 99乘法表的几种实现代码
2020/10/13 PHP
网页设计常用的一些技巧
2006/12/22 Javascript
完美解决JS中汉字显示乱码问题(已解决)
2006/12/27 Javascript
JavaScript 利用StringBuffer类提升+=拼接字符串效率
2009/11/24 Javascript
JS 面向对象之神奇的prototype
2011/02/26 Javascript
js模拟hashtable的简单实例
2014/03/06 Javascript
jQuery实现回车键(Enter)切换文本框焦点的代码实例
2014/05/05 Javascript
javascript中innerText和innerHTML属性用法实例分析
2015/05/13 Javascript
JQuery实现样式设置、追加、移除与切换的方法
2015/06/11 Javascript
基于KO+BootStrap+MVC实现的分页控件代码分享
2016/11/07 Javascript
jQuery事件_动力节点Java学院整理
2017/07/05 jQuery
微信小程序实现倒计时补零功能
2018/07/09 Javascript
浅谈ElementUI中switch回调函数change的参数问题
2018/08/24 Javascript
Element-ui中元素滚动时el-option超出元素区域的问题
2019/05/30 Javascript
JS面向对象编程基础篇(二) 封装操作实例详解
2020/03/03 Javascript
JS实现电脑虚拟键盘的操作
2020/06/24 Javascript
解决vue的router组件component在import时不能使用变量问题
2020/07/26 Javascript
详解微信小程序动画Animation执行过程
2020/09/23 Javascript
[03:17]2014DOTA2 国际邀请赛中国区预选赛 四强专访
2014/05/23 DOTA
python里将list中元素依次向前移动一位
2014/09/12 Python
深入浅析Python的类
2018/06/22 Python
django 中使用DateTime常用的时间查询方式
2019/12/03 Python
python3 webp转gif格式的实现示例
2019/12/10 Python
爱与责任演讲稿
2014/05/20 职场文书
公益广告标语
2014/06/19 职场文书
图书馆志愿者活动总结
2014/06/27 职场文书
2014年学校安全工作总结
2014/11/13 职场文书
小学优秀教师材料
2014/12/15 职场文书
2014年学生资助工作总结
2014/12/18 职场文书
新课程改革心得体会
2016/01/22 职场文书
浅谈Go语言多态的实现与interface使用
2021/06/16 Golang
Redis安装使用RedisJSON模块的方法
2022/03/23 Redis