在计算机科学中,柯里化(英语:Currying
),又译为卡瑞化
或加里化
,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇
以逻辑学家哈斯凯尔·加里
命名的,尽管它是Moses Schönfinkel
和戈特洛布·弗雷格发明的
。javascript
在直觉上,柯里化声称若是你固定某些参数,你将获得接受余下参数的一个函数。
在理论计算机科学中,柯里化提供了在简单的理论模型中,好比:只接受一个单一参数的lambda
演算中,研究带有多个参数的函数的方式。
函数柯里化的对偶是Uncurrying
,一种使用匿名单参数函数来实现多参数函数的方法。java
Currying概念其实很简单,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。git
若是咱们须要实现一个求三个数之和的函数:github
function add(x, y, z) { return x + y + z; } console.log(add(1, 2, 3)); // 6
var add = function(x) { return function(y) { return function(z) { return x + y + z; } } } var addOne = add(1); var addOneAndTwo = addOne(2); var addOneAndTwoAndThree = addOneAndTwo(3); console.log(addOneAndTwoAndThree);
这里咱们定义了一个add
函数,它接受一个参数并返回一个新的函数。调用add
以后,返回的函数就经过闭包的方式记住了add
的第一个参数。一次性地调用它实在是有点繁琐,好在咱们可使用一个特殊的curry
帮助函数(helper function
)使这类函数的定义和调用更加容易。ajax
用ES6
的箭头函数,咱们能够将上面的add
实现成这样:设计模式
const add = x => y => z => x + y + z;
好像使用箭头函数更清晰了许多。数组
来看这个函数:闭包
function ajax(url, data, callback) { // .. }
有这样的一个场景:咱们须要对多个不一样的接口发起HTTP
请求,有下列两种作法:app
ajax()
函数时,传入全局URL
常量。URL
实参的函数引用。下面咱们建立一个新函数,其内部仍然发起ajax()
请求,此外在等待接收另外两个实参的同时,咱们手动将ajax()
第一个实参设置成你关心的API
地址。函数
对于第一种作法,咱们可能产生以下调用方式:
function ajaxTest1(data, callback) { ajax('http://www.test.com/test1', data, callback); } function ajaxTest2(data, callback) { ajax('http://www.test.com/test2', data, callback); }
对于这两个相似的函数,咱们还能够提取出以下的模式:
function beginTest(callback) { ajaxTest1({ data: GLOBAL_TEST_1, }, callback); }
相信您已经看到了这样的模式:咱们在函数调用现场(function call-site
),将实参应用(apply
) 于形参。如你所见,咱们一开始仅应用了部分实参 —— 具体是将实参应用到URL
形参 —— 剩下的实参稍后再应用。
上述概念即为偏函数的定义,偏函数一个减小函数参数个数的过程;这里的参数个数指的是但愿传入的形参的数量。咱们经过ajaxTest1()
把原函数ajax()
的参数个数从3
个减小到了2
个。
咱们这样定义一个partial()
函数:
function partial(fn, ...presetArgs) { return function partiallyApplied(...laterArgs) { return fn(...presetArgs, ...laterArgs); } }
partial()
函数接收fn
参数,来表示被咱们偏应用实参(partially apply
)的函数。接着,fn
形参以后,presetArgs
数组收集了后面传入的实参,保存起来稍后使用。
咱们建立并return
了一个新的内部函数(为了清晰明了,咱们把它命名为partiallyApplied(..)
),该函数中,laterArgs
数组收集了所有实参。
使用箭头函数,则更为简洁:
var partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
使用偏函数的这种模式,咱们重构以前的代码:
function ajax(url, data, callback) { // .. } var ajaxTest1 = partial(ajax, 'http://www.test.com/test1'); var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');
再次思考beginTest()
函数,咱们使用partial()
来重构它应该怎么作呢?
function ajax(url, data, callback) { // .. } // 版本1 var beginTest = partial(ajax, 'http://www.test.com/test1', { data: GLOBAL_TEST_1, }); // 版本2 var ajaxTest1 = partial(ajax, 'http://www.test.com/test1'); var beginTest = partial(ajaxTest1, { data: GLOBAL_TEST_1, });
相信你已经在上述例子中看到了版本2比起版本1的优点所在了,没错,柯里化就是:将一个带有多个参数的函数转换为一次一个的函数的过程。每次调用函数时,它只接受一个参数,并返回一个函数,直到传递全部参数为止。
The process of converting a function that takes multiple arguments into a function that takes them one at a time.
Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.
假设咱们已经建立了一个柯里化版本的ajax()
函数curriedAjax()
:
curriedAjax('http://www.test.com/test1') ({ data: GLOBAL_TEST_1, }) (function callback(data) { // dosomething });
咱们将三次调用分别拆解开来,这也许有助于咱们理解整个过程:
var ajaxTest1 = curriedAjax('http://www.test.com/test1'); var beginTest = ajaxTest1({ data: GLOBAL_TEST_1, }); var ajaxCallback = beginTest(function callback(data) { // dosomething });
那么,咱们如何来实现一个自动的柯里化的函数呢?
var currying = function(fn) { var args = []; return function() { if (arguments.length === 0) { return fn.apply(this, args); // 没传参数时,调用这个函数 } else { [].push.apply(args, arguments); // 传入了参数,把参数保存下来 return arguments.callee; // 返回这个函数的引用 } } }
调用上述currying()
函数:
var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var cost = currying(cost); cost(100); // 传入了参数,不真正求值 cost(200); // 传入了参数,不真正求值 cost(300); // 传入了参数,不真正求值 console.log(cost()); // 求值而且输出600
上述函数是我以前的JavaScript设计模式与开发实践读书笔记之闭包与高阶函数所写的currying
版本,如今仔细思考后发现仍旧有一些问题。
咱们在使用柯里化时,要注意同时为函数预传的参数的状况。
所以把上述柯里化函数更改以下:
var currying = function(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { if (arguments.length === 0) { return fn.apply(this, args); // 没传参数时,调用这个函数 } else { [].push.apply(args, arguments); // 传入了参数,把参数保存下来 return arguments.callee; // 返回这个函数的引用 } } }
使用实例:
var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var cost = currying(cost, 100); cost(200); // 传入了参数,不真正求值 cost(300); // 传入了参数,不真正求值 console.log(cost()); // 求值而且输出600
你可能会以为每次都要在最后调用一下不带参数的cost()
函数比较麻烦,而且在cost()
函数都要使用arguments
参数不符合你的预期。咱们知道函数都有一个length
属性,代表函数指望接受的参数个数。所以咱们能够充分利用预传参数的这个特色。
借鉴自mqyqingfeng:
function sub_curry(fn) { var args = [].slice.call(arguments, 1); return function() { return fn.apply(this, args.concat([].slice.call(arguments))); }; } function curry(fn, length) { length = length || fn.length; var slice = Array.prototype.slice; return function() { if (arguments.length < length) { var combined = [fn].concat(slice.call(arguments)); return curry(sub_curry.apply(this, combined), length - arguments.length); } else { return fn.apply(this, arguments); } }; }
在上述函数中,咱们在currying的返回函数中,每次把arguments.length
和fn.length
做比较,一旦arguments.length
达到了fn.length
的数量,咱们就去调用fn
(return fn.apply(this, arguments);
)
验证:
var fn = curry(function(a, b, c) { return [a, b, c]; }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]
使用柯里化,可以很方便地借用call()
或者apply()
实现bind()
方法的polyfill
。
Function.prototype.bind = Function.prototype.bind || function(context) { var me = this; var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return me.apply(contenxt, finalArgs); } }
上述函数有的问题在于不能兼容构造函数。咱们经过判断this指向的对象的原型属性,来判断这个函数是否经过new
做为构造函数调用,来使得上述bind
方法兼容构造函数。
Function.prototype.bind() by MDN以下说到:
绑定函数适用于用new操做符 new 去构造一个由目标函数建立的新的实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。然而, 原先提供的那些参数仍然会被前置到构造函数调用的前面。
这是基于MVC的JavaScript Web富应用开发的bind()
方法实现:
Function.prototype.bind = function(oThis) { if (typeof this !== "function") { throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { return fToBind.apply( this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };
可能遇到这种状况:拿到一个柯里化后的函数,却想要它柯里化以前的版本,这本质上就是想将相似f(1)(2)(3)
的函数变回相似g(1,2,3)
的函数。
下面是简单的uncurrying
的实现方式:
function uncurrying(fn) { return function(...args) { var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret(args[i]); // 反复调用currying版本的函数 } return ret; // 返回结果 }; }
注意,不要觉得uncurrying后的函数和currying以前的函数如出一辙,它们只是行为相似!
var currying = function(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { if (arguments.length === 0) { return fn.apply(this, args); // 没传参数时,调用这个函数 } else { [].push.apply(args, arguments); // 传入了参数,把参数保存下来 return arguments.callee; // 返回这个函数的引用 } } } function uncurrying(fn) { return function(...args) { var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret(args[i]); // 反复调用currying版本的函数 } return ret; // 返回结果 }; } var cost = (function() { var money = 0; return function() { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; } })(); var curryingCost = currying(cost); var uncurryingCost = uncurrying(curryingCost); console.log(uncurryingCost(100, 200, 300)()); // 600
不管是柯里化仍是偏应用,咱们都能进行部分传值,而传统函数调用则须要预先肯定全部实参。若是你在代码某一处只获取了部分实参,而后在另外一处肯定另外一部分实参,这个时候柯里化和偏应用就能派上用场。
另外一个最能体现柯里化应用的的是,当函数只有一个形参时,咱们可以比较容易地组合它们(单一职责原则(Single responsibility principle)
)。所以,若是一个函数最终须要三个实参,那么它被柯里化之后会变成须要三次调用,每次调用须要一个实参的函数。当咱们组合函数时,这种单元函数的形式会让咱们处理起来更简单。
概括下来,主要为如下常见的三个用途: