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