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 相关文章推荐
javascript 单例/单体模式(Singleton)
Apr 07 Javascript
jQuery侧边栏随窗口滚动实现方法
Mar 04 Javascript
jquery 实现窗口的最大化不论什么情况
Sep 03 Javascript
在javaScript中关于submit和button的区别介绍
Oct 20 Javascript
Node.js中的事件驱动编程详解
Aug 16 Javascript
CSS3 media queries结合jQuery实现响应式导航
Sep 30 Javascript
AngularJS模板加载用法详解
Nov 04 Javascript
pm2 部署 node的三种方法示例
Oct 20 Javascript
javascript计算对象长度的方法
Oct 25 Javascript
vue页面跳转后返回原页面初始位置方法
Feb 11 Javascript
vue实现员工信息录入功能
Jun 11 Javascript
node使用async_hooks模块进行请求追踪
Jan 28 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实现详细解析
2013/08/24 PHP
php实现基于PDO的预处理示例
2017/03/28 PHP
PHP工厂模式的日常使用
2019/03/20 PHP
js下将字符串当函数执行的方法
2011/07/13 Javascript
js 对小数加法精度处理示例说明
2013/12/27 Javascript
JavaScript实现cookie的写入、读取、删除功能
2015/11/05 Javascript
js读取并解析JSON类型数据的方法
2015/11/14 Javascript
微信js-sdk界面操作接口用法示例
2016/10/12 Javascript
Jquery UI实现一次拖拽多个选中的元素操作
2020/12/01 Javascript
Ajax跨域实现代码(后台jsp)
2017/01/21 Javascript
js Canvas绘制圆形时钟教程
2017/02/06 Javascript
jQuery插件HighCharts实现的2D回归直线散点效果示例【附demo源码下载】
2017/03/09 Javascript
浅谈vue.js中v-for循环渲染
2017/07/26 Javascript
详解vue-cli官方脚手架配置
2018/07/20 Javascript
Electron中实现大文件上传和断点续传功能
2018/10/28 Javascript
微信小程序页面传多个参数跳转页面的实现方法
2019/05/17 Javascript
Nuxt的动态路由和参数校验操作
2020/11/09 Javascript
Python库urllib与urllib2主要区别分析
2014/07/13 Python
python爱心表白 每天都是浪漫七夕!
2018/08/18 Python
Python中如何使用if语句处理列表实例代码
2019/02/24 Python
Python 函数返回值的示例代码
2019/03/11 Python
浅谈Python_Openpyxl使用(最全总结)
2019/09/05 Python
Python unittest装饰器实现原理及代码
2020/09/08 Python
CSS3属性使网站设计增强同时不消弱可用性
2009/08/29 HTML / CSS
STAUD官方网站:洛杉矶独有的闲适风格
2019/04/11 全球购物
圣彼得堡鲜花配送:Semicvetic
2020/09/15 全球购物
个人简历自荐信
2013/12/05 职场文书
大学生文员专业个人求职信范文
2014/01/05 职场文书
事业单位接收函
2014/01/10 职场文书
教师研修随笔感言
2014/01/23 职场文书
社区安全检查制度
2014/02/03 职场文书
社会实践活动总结报告
2014/04/29 职场文书
汽车修理厂管理制度
2015/08/05 职场文书
交通安全主题班会
2015/08/12 职场文书
python数据库批量插入数据的实现(executemany的使用)
2021/04/30 Python
浅谈自定义校验注解ConstraintValidator
2021/06/30 Java/Android