在计算机科学中,柯里化 (Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
理论看着头大?不要紧,先看看代码:编程
假设咱们须要实现一个对列表元素进行某种处理的功能,好比说返回一个原列表内每个元素加一的新列表,那么很容易想到:数组
const list = [0, 1, 2, 3]; const list1 = list.map(elem => elem + 1); // => [1, 2, 3, 4]
很简单是吧?若是又要加2呢?闭包
const list = [0, 1, 2, 3]; const list1 = list.map(elem => elem + 1); // => [1, 2, 3, 4] const list2 = list.map(elem => elem + 2); // => [2, 3, 4, 5]
看上去效率有点低,处理函数封装下?
但是map的回调函数只接受当前元素 elem 这一个参数,看上去好像没有办法封装...app
你也许会想:若是能拿到一个部分配置好的函数就行了,好比说:函数式编程
// plus返回部分配置好的函数 const plus1 = plus(1); const plus2 = plus(2); plus1(5); // => 6 plus2(7); // => 9
把这样的函数传进map:函数
const list = [0, 1, 2, 3]; const list1 = list.map(plus1); // => [1, 2, 3, 4] const list2 = list.map(plus2); // => [2, 3, 4, 5]
是否是很棒棒?这样一来不论是加多少,只须要list.map(plus(x))
就行了,完美实现了封装,可读性大大提升! (☆゚∀゚)性能
不过问题来了:
这样的plus函数要怎么实现呢?测试
这时候柯里化就能派上用场了:rest
// 原始的加法函数 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; }
,这样就实现了部分配置。code
通俗地讲,柯里化就是一个部分配置多参数函数的过程,每一步都返回一个接受单个参数的部分配置好的函数。一些极端的状况可能须要分不少次来部分配置一个函数,好比说屡次相加:
multiPlus(1)(2)(3); // => 6
这种写法看着很奇怪吧?不过若是入了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
这样就实现了自动的柯里化!(╭ ̄3 ̄)╭♡
若是你看得懂发生了什么的话,那么恭喜你!你们口中的大佬就是你!╰(°▽°)╯,快留下赞而后去开始你的函数式生涯吧(滑稽
若是你没看懂发生了什么,别担忧,我如今开始帮你理一下思路。
咱们须要一个 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; }
与本篇文章先前提到的方法对比的话,发现这种方法有两个问题:
作个测试:
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)上:
差的这一点猜想是闭包的缘由。因为闭包的访问比较耗性能,而这种方式造成了两个闭包:fn 和 len,前面提到的方法只造成了 fn 一个闭包,因此形成了这一微小的差距。
也但愿你们能本身测试下并说说本身的见解~
有问题欢迎留言~ ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄.
<!-- End -->