柯里化与反柯里化
最近在看一本书《JavaScript函数式编程》
里边提到了一个名词,柯里化(currying),阅读后发现在日常开发中经常会用到柯里化函数。
以及还有他的反义词反柯里化(unCurrying)柯里化被称为部分计算函数,也就是会固定一部分参数,然后返回一个接收剩余参数的函数。目的是为了缩小适用范围,创建一个针对性更强的函数。反柯里化正好与之相反,我们是要扩大一个函数的适用范围,比如将Array独有的push应用到一个Object上去。
两种方案的通用代码实现
1 | function currying (func, ...preArgs) { |
两种方案的简单示意
currying
1 | foo(arg1, arg2) |
unCurrying
1 | obj.foo(arg1, arg2) |
柯里化currying
一个柯里化函数的简单应用,我们有一个进行三个参数求和的函数。
我们可以调用currying传入sum获得sum1,一个固定了第一个参数为10的求和函数
然后我们又调用currying传入sum1获得sum2,在原有的固定了一个参数的基础上,再次固定一个参数20
这时我们调用sum2时仅需传入一个参数即可完成三个参数的求和:10 + 20 + n
1 | let sum = (a, b, c) => a + b + c // 一个进行三个参数求和的函数 |
帮助人理解currying最简单的例子就是XXX.bind(this, yourArgs)()
写过React的人应该都知道,在一些列表需要绑定事件时,我们大致会有这样的代码:
1 | { |
这样我们就能在点击事件被触发时拿到对应的ID了。这其实就是一个函数柯里化的操作
我们通过bind生成了多个函数,每个函数都固定了第一个参数index,然后第二个参数才是event对象。
又或者我们有如下结构的数据,我们需要新增一列数据的展示description,要求格式为所在部门-姓名。
1 | const data = [{ |
如果用普通函数的处理方法,可能是这样的:
1 | let result = data.map(sections => { |
或者我们可以用currying的方式来实现
1 | let result = data.map(sections => { |
使用柯里化还有一种好处,就是可以帮助我们明确调用函数的参数。
我们创建一个如下函数,一个看似非常鸡肋的函数,大致作用如下:
- 接收一个函数
- 返回一个只接收一个参数的函数
1 | function curry (func) { |
我们应该都用过一个全局函数parseInt
用来将String转换为NumberparseInt('10') // 10
但其实,parseInt不止接收一个参数。parseInt('10', 2) // 2
第二个参数可以用来标识给定值的基数,告诉我们用N进制来处理这个字符串
所以当我们直接将一个parseInt传入map中时就会遇到一些问题:
1 | ['1', '2', '3', '4'].map(parseInt) // => 1, NaN, NaN, NaN |
因为map回调的返回值有三个参数当前item、当前item对应的index、调用map的对象引用
所以我们可以用上边的curry函数来解决这个问题,限制parseInt只接收一个参数
1 | ['1', '2', '3', '4'].map(curry(parseInt)) // => 1, 2, 3, 4 |
缩小适用范围,创建一个针对性更强的函数
反柯里化unCurrying
虽说名字叫反柯里化。。但是我觉得也只是部分理念上相反,而不是向
Math.max和Math.min,又或者[].pop和[].push这样的完全相反。
就像柯里化是缩小了适用范围,所以反柯里化所做的就是扩大适用范围。
这个在开发中也会经常用到,比如某宝有一个经典的面试题:
如何获取一个页面中所用到的所有标签,并将其输出?
1 | // 普通函数的写法 |
因为qsa返回的是一个NodeList对象,一个类数组的对象,他是没有直接实现map方法的。
而反柯里化就是用来帮助它实现这个的,扩大适用范围,让一些原本无法调用的函数变得可用
1 | let map = unCurrying([].map) |
又或者早期写JavaScript时对arguments对象的处理,这也是一个类数组对象。
比如一些早期版本的currying函数实现(手动斜眼):
1 | function old_currying () { |
里边用到的[].slice.call经过一层封装后,其实就是实现的unCurrying的效果
网上流传的一个有趣的面试题
有大概这么一道题,如何实现下面的函数:
1 | var a = func(1) |
这里是一个实现的方案:https://github.com/Jiasm/notebook/blob/master/currying.js
一个柯里化实现的变体。
小记
在《JavaScript函数式编程》中提到了,高阶函数的几个特性:
- 以一个函数作为参数
- 以一个函数作为返回值
柯里化/反柯里化只是其中的一小部分。
其实柯里化还分为了向右柯里化、向左柯里化(大概就是preArgs和args的调用顺序问题了)
用函数构建出新的函数,将函数组合在一起,这个是贯穿这本书的一个理念,在现在大量的面向对象编程开发中,能够看到这么一本书,感觉很是清新。从另一个角度看待JavaScript这门语言,强烈推荐,值得一看。
文章部分示例代码:https://github.com/Jiasm/currying-uncurrying