Haskell과 scala는 모두 함수의 커링을 지원합니다. JavaScript 함수의 커링도 JavaScript 함수형 프로그래밍과 밀접한 관련이 있습니다. 관심이 있으시면 이러한 측면을 더 많이 이해하시면 도움이 될 것입니다.
이 글을 읽기 위해 알아야 할 몇 가지 지식 포인트
call
/apply
/arguments
call
/apply
/arguments
文章后面有对这些知识的简单解释,大家可以看看.
我们先来看看维基百科中是如何定义的:在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
我们可以举个简单的例子,如下函数add
是一般的一个函数,就是将传进来的参数a
和b
相加;函数curryingAdd
就是对函数add
进行柯里化的函数;
这样一来,原来我们需要直接传进去两个参数来进行运算的函数,现在需要分别传入参数a
和b
,函数如下:
function add(a, b) { return a + b; } function curryingAdd(a) { return function(b) { return a + b; } } add(1, 2); // 3 curryingAdd(1)(2); // 3
看到这里你可能会想,这样做有什么用?为什么要这样做?这样做能够给我们的应用带来什么样的好处?先别着急,我们接着往下看.
总之,函数的柯里化能够让你重新组合你的应用,把你的复杂功能拆分成一个一个的小部分,每一个小的部分都是简单的,便于理解的,而且是容易测试的;
在这一部分里,我们由浅入深的一步步来告诉大家如何对一个多参数的函数进行柯里化.其中用到的知识有闭包
,高阶函数
,不完全函数
等等.
I 开胃菜
假如我们要实现一个功能,就是输出语句name
喜欢song
,其中name
和song
都是可变参数;那么一般情况下我们会这样写:
function printInfo(name, song) { console.log(name + '喜欢的歌曲是: ' + song); } printInfo('Tom', '七里香'); printInfo('Jerry', '雅俗共赏');
对上面的函数进行柯里化之后,我们可以这样写:
function curryingPrintInfo(name) { return function(song) { console.log(name + '喜欢的歌曲是: ' + song); } } var tomLike = curryingPrintInfo('Tom'); tomLike('七里香'); var jerryLike = curryingPrintInfo('Jerry'); jerryLike('雅俗共赏');
II 小鸡炖蘑菇
上面我们虽然对对函数printInfo
进行了柯里化,但是我们可不想在需要柯里化的时候,都像上面那样不断地进行函数的嵌套,那简直是噩梦;
所以我们要创造一些帮助其它函数进行柯里化的函数,我们暂且叫它为curryingHelper
吧,一个简单的curryingHelper
函数如下所示:
function curryingHelper(fn) { var _args = Array.prototype.slice.call(arguments, 1); return function() { var _newArgs = Array.prototype.slice.call(arguments); var _totalArgs = _args.concat(_newArgs); return fn.apply(this, _totalArgs); } }
这里解释一点东西,首先函数的arguments
表示的是传递到函数中的参数对象,它不是一个数组,它是一个类数组对象;
所以我们可以使用函数的Array.prototype.slice
方法,然后使用.call
方法来获取arguments
里面的内容.
我们使用fn.apply(this, _totalArgs)
来给函数fn
传递正确的参数.
接下来我们来写一个简单的函数验证上面的辅助柯里化函数的正确性, 代码部分如下:
function showMsg(name, age, fruit) { console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit); } var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple'); curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20); curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old, and I like eat watermelon
上面的结果表示,我们的这个柯里化的函数是正确的.上面的curryingHelper
就是一个高阶函数,关于高阶函数的解释可以参照下文.
III 牛肉火锅
上面的柯里化帮助函数确实已经能够达到我们的一般性需求了,但是它还不够好,我们希望那些经过柯里化后的函数可以每次只传递进去一个参数,
然后可以进行多次参数的传递,那么应该怎么办呢?我们可以再花费一些脑筋,写出一个betterCurryingHelper
函数,实现我们上面说的那些
功能.代码如下:
function betterCurryingHelper(fn, len) { var length = len || fn.length; return function () { var allArgsFulfilled = (arguments.length >= length); // 如果参数全部满足,就可以终止递归调用 if (allArgsFulfilled) { return fn.apply(this, arguments); } else { var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments)); return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length); } }; }
其中curryingHelper
就是上面II 小鸡炖蘑菇中提及的那个函数.需要注意的是fn.length
表示的是这个函数的参数长度.
接下来我们来检验一下这个函数的正确性:
var betterShowMsg = betterCurryingHelper(showMsg); betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
其中showMsg
就是II 小鸡炖蘑菇部分提及的那个函数.
我们可以看出来,这个betterCurryingHelper
add
는 a
및 b
매개변수를 전달하는 일반 함수입니다. ; curryingAdd
함수는 add
함수를 커리하는 함수입니다.a
와 b
를 각각 전달해야 합니다. 함수는 다음과 같습니다. 🎜var _ = {}; function crazyCurryingHelper(fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function() { var _args = args.slice(), _holes = holes.slice(); // 存储接收到的args和holes的长度 var argLength = _args.length, holeLength = _holes.length; var allArgumentsSpecified = false; // 循环 var arg = null, i = 0, aLength = arguments.length; for(; i < aLength; i++) { arg = arguments[i]; if(arg === _ && holeLength) { // 循环holes的位置 holeLength--; _holes.push(_holes.shift()); } else if (arg === _) { // 存储hole就是_的位置 _holes.push(argLength + i); } else if (holeLength) { // 是否还有没有填补的hole // 在参数列表指定hole的地方插入当前参数 holeLength--; _args.splice(_holes.shift(), 0, arg); } else { // 不需要填补hole,直接添加到参数列表里面 _args.push(arg); } } // 判断是否所有的参数都已满足 allArgumentsSpecified = (_args.length >= length); if(allArgumentsSpecified) { return fn.apply(this, _args); } // 递归的进行柯里化 return crazyCurryingHelper.call(this, fn, length, _args, _holes); }; }🎜이것을 보면 무엇인지 생각할 수 있습니다. 이것을 사용하는 이유는 무엇입니까? 이것이 우리 애플리케이션에 어떤 이점을 가져올 수 있습니까? 걱정하지 마세요. 🎜🎜카레 기능이 필요한 이유는 무엇입니까? (아래 참조)🎜 🎜미리 함수에 특정 매개변수를 바인딩하면 매개변수 재사용 효과를 얻고 적용성이 향상됩니다. 🎜🎜가변 요소 고정 🎜🎜지연 계산🎜🎜간단히 말하면, 함수 커링을 사용하면 재구성이 가능합니다. 복잡한 기능을 작은 부분으로 나누세요. 각각의 작은 부분은 간단하고 이해하기 쉽고 테스트하기 쉽습니다. 🎜🎜함수를 카레하는 방법 🎜🎜이 부분에서는 카레를 만드는 방법을 단계별로 알려드리겠습니다. 다중 매개변수 함수에는
클로저
, 고차 함수
, 불완전 함수
등이 포함됩니다. 🎜🎜🎜🎜🎜I appetizer🎜 🎜🎜함수를 구현하려면 song
과 같이 name
문을 출력하면 됩니다. 여기서 name
및 song code>는 둘 다 변수 매개변수입니다. 그러면 일반적으로 다음과 같이 작성합니다. 🎜<pre class="brush:php;toolbar:false">var crazyShowMsg = crazyCurryingHelper(showMsg);
crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple</pre>🎜위 함수를 구성합니다. 카레를 사용한 후 다음과 같이 작성할 수 있습니다. 위의 <code>printInfo
에서는 카레가 필요할 때 카레를 사용하고 싶지 않습니다. 경화할 때 위와 같은 함수를 계속 중첩하는 것은 악몽입니다.curryingHelper
라고 부르자. 간단한 curryingHelper
함수는 다음과 같다. 🎜function hello(name) { console.log('Hello, ' + name); } setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行 setTimeout(function() { hello('dreamapple'); }, 3600); // 3.6s 后执行🎜 먼저
에 대해 설명해보자. 함수의 >인수
는 함수에 전달된 매개변수를 나타냅니다. 객체는 배열이 아니며 배열과 유사한 객체입니다.Array.prototype을 사용할 수 있습니다. 슬라이스
메소드를 사용하고 .call
메소드를 사용하여 인수
의 내용을 가져옵니다.fn.apply(this, _totalArgs)
를 사용하여 올바른 매개변수를 fn
함수에 전달합니다.🎜🎜다음으로 위의 보조 커링 함수의 정확성을 확인하는 간단한 함수를 작성해 보겠습니다. 코드 부분은 다음과 같습니다. 🎜setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数🎜위 결과는 위의
curryingHelper
가 🎜고차 함수🎜임을 나타냅니다. 고차 함수에 대한 설명은 다음을 참조하세요.🎜🎜 🎜🎜🎜III Beef Hot Pot🎜🎜🎜위의 카레 도우미 함수는 실제로 우리의 목표를 달성할 수 있습니다. 일반적인 요구 사항이지만 충분하지 않습니다. br/>그러면 여러 매개변수를 전달할 수 있습니다. 그러면 우리는 무엇을 해야 할까요? 위에서 언급한 betterCurryingHelper
함수를 작성하면 됩니다. 🎜setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的🎜여기서
curryingHelper
는 위 🎜II 버섯을 곁들인 닭고기 조림🎜에서 언급한 함수입니다. fn.length
는 의 매개변수 길이를 나타냅니다. 이 함수입니다.function multiply(x) { var y = function(x) { return multiply(x * y); }; y.toString = y.valueOf = function() { return x; }; return y; } console.log(multiply(1)(2)(3) == 6); // true console.log(multiply(1)(2)(3)(4)(5) == 120); // true🎜여기서
showMsg
는 🎜II 버섯 닭고기 조림🎜 부분에서 언급한 함수입니다.betterCurryingHelper
가 실제로 우리가 원하는 기능을 구현한 것을 볼 수 있습니다. 그리고 원래 기능처럼 카레 기능을 사용할 수도 있습니다.🎜🎜🎜🎜🎜IV 절인 고추 닭발🎜🎜我们已经能够写出很好的柯里化辅助函数了,但是这还不算是最刺激的,如果我们在传递参数的时候可以不按照顺来那一定很酷;当然我们也可以写出这样的函数来,
这个crazyCurryingHelper
函数如下所示:
var _ = {}; function crazyCurryingHelper(fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function() { var _args = args.slice(), _holes = holes.slice(); // 存储接收到的args和holes的长度 var argLength = _args.length, holeLength = _holes.length; var allArgumentsSpecified = false; // 循环 var arg = null, i = 0, aLength = arguments.length; for(; i < aLength; i++) { arg = arguments[i]; if(arg === _ && holeLength) { // 循环holes的位置 holeLength--; _holes.push(_holes.shift()); } else if (arg === _) { // 存储hole就是_的位置 _holes.push(argLength + i); } else if (holeLength) { // 是否还有没有填补的hole // 在参数列表指定hole的地方插入当前参数 holeLength--; _args.splice(_holes.shift(), 0, arg); } else { // 不需要填补hole,直接添加到参数列表里面 _args.push(arg); } } // 判断是否所有的参数都已满足 allArgumentsSpecified = (_args.length >= length); if(allArgumentsSpecified) { return fn.apply(this, _args); } // 递归的进行柯里化 return crazyCurryingHelper.call(this, fn, length, _args, _holes); }; }
一些解释,我们使用_
来表示参数中的那些缺失的参数,如果你使用了lodash的话,会有冲突的;那么你可以使用别的符号替代.
按照一贯的尿性,我们还是要验证一下这个crazyCurryingHelper
是不是实现了我们所说的哪些功能,代码如下:
var crazyShowMsg = crazyCurryingHelper(showMsg); crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
结果显示,我们这个函数也实现了我们所说的那些功能.
说了那么多,其实这部分才是最重要的部分;学习某个知识要一定可以用得到,不然学习它干嘛.
关于函数柯里化的一些小技巧
给setTimeout
传递地进来的函数添加参数
一般情况下,我们如果想给一个setTimeout
传递进来的函数添加参数的话,一般会使用之种方法:
function hello(name) { console.log('Hello, ' + name); } setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行 setTimeout(function() { hello('dreamapple'); }, 3600); // 3.6s 后执行
我们使用了一个新的匿名函数包裹我们要执行的函数,然后在函数体里面给那个函数传递参数值.
当然,在ES5里面,我们也可以使用函数的bind
方法,如下所示:
setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数
这样也是非常的方便快捷,并且可以绑定函数执行的上下文.
我们本篇文章是讨论函数的柯里化,当然我们这里也可以使用函数的柯里化来达到这个效果:
setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的
这样也是可以的,是不是很酷.其实函数的bind
方法也是使用函数的柯里化来完成的,详情可以看这里Function.prototype.bind().
写出这样一个函数multiply(1)(2)(3) == 6
结果为true
,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)
结果为true
这个题目不知道大家碰到过没有,不过通过函数的柯里化,也是有办法解决的,看下面的代码:
function multiply(x) { var y = function(x) { return multiply(x * y); }; y.toString = y.valueOf = function() { return x; }; return y; } console.log(multiply(1)(2)(3) == 6); // true console.log(multiply(1)(2)(3)(4)(5) == 120); // true
因为multiply(1)(2)(3)
的直接结果并不是6,而是一个函数对象{ [Number: 6] valueOf: [Function], toString: [Function] }
,我们
之后使用了==
会将左边这个函数对象转换成为一个数字,所以就达到了我们想要的结果.还有关于为什么使用toString
和valueOf
方法
可以看看这里的解释Number.prototype.valueOf(),Function.prototype.toString().
上面的那个函数不够纯粹,我们也可以实现一个更纯粹的函数,但是可以会不太符合题目的要求.
我们可以这样做,先把函数的参数存储,然后再对这些参数做处理,一旦有了这个思路,我们就不难写出些面的代码:
function add() { var args = Array.prototype.slice.call(arguments); var _that = this; return function() { var newArgs = Array.prototype.slice.call(arguments); var total = args.concat(newArgs); if(!arguments.length) { var result = 1; for(var i = 0; i < total.length; i++) { result *= total[i]; } return result; } else { return add.apply(_that, total); } } } add(1)(2)(3)(); // 6 add(1, 2, 3)(); // 6
当我们的需要兼容IE9之前版本的IE浏览器的话,我们可能需要写出一些兼容的方案 ,比如事件监听;一般情况下我们应该会这样写:
var addEvent = function (el, type, fn, capture) { if (window.addEventListener) { el.addEventListener(type, fn, capture); } else { el.attachEvent('on' + type, fn); } };
这也写也是可以的,但是性能上会差一点,因为如果是在低版本的IE浏览器上每一次都会运行if()
语句,产生了不必要的性能开销.
我们也可以这样写:
var addEvent = (function () { if (window.addEventListener) { return function (el, type, fn, capture) { el.addEventListener(type, fn, capture); } } else { return function (el, type, fn) { var IEtype = 'on' + type; el.attachEvent(IEtype, fn); } } })();
这样就减少了不必要的开支,整个函数运行一次就可以了.
延迟计算
上面的那两个函数multiply()
和add()
实际上就是延迟计算的例子.
提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.
我们的I 开胃菜
部分的tomLike
和jerryLike
其实就是属于这种的,绑定好函数里面的第一个参数,然后后面根据情况分别使用不同的函数.
固定易变因素
我们经常使用的函数的bind
方法就是一个固定易变因素的很好的例子.
当然,使用柯里化意味着有一些额外的开销;这些开销一般涉及到这些方面,首先是关于函数参数的调用,操作arguments
对象通常会比操作命名的参数要慢一点;
还有,在一些老的版本的浏览器中arguments.length
的实现是很慢的;直接调用函数fn
要比使用fn.apply()
或者fn.call()
要快一点;产生大量的嵌套
作用域还有闭包会带来一些性能还有速度的降低.但是,大多数的web应用的性能瓶颈时发生在操作DOM上的,所以上面的那些开销比起DOM操作的开销还是比较小的.
琐碎的知识点
fn.length
: 表示的是这个函数中参数的个数.
arguments.callee
: 指向的是当前运行的函数.callee
是arguments
对象的属性。
在该函数的函数体内,它可以指向当前正在执行的函数.当函数是匿名函数时,这是很有用的,比如没有名字的函数表达式(也被叫做"匿名函数").
详细解释可以看这里arguments.callee.我们可以看一下下面的例子:
function hello() { return function() { console.log('hello'); if(!arguments.length) { console.log('from a anonymous function.'); return arguments.callee; } } } hello()(1); // hello /* * hello * from a anonymous function. * hello * from a anonymous function. */ hello()()();
fn.caller
: 返回调用指定函数的函数.详细的解释可以看这里Function.caller,下面是示例代码:
function hello() { console.log('hello'); console.log(hello.caller); } function callHello(fn) { return fn(); } callHello(hello); // hello [Function: callHello]
高阶函数(high-order function)
高阶函数就是操作函数的函数,它接受一个或多个函数作为参数,并返回一个新的函数.
我们来看一个例子,来帮助我们理解这个概念.就举一个我们高中经常遇到的场景,如下:
f1(x, y) = x + y; f2(x) = x * x; f3 = f2(f3(x, y));
我们来实现f3
函数,看看应该如何实现,具体的代码如下所示:
function f1(x, y) { return x + y; } function f2(x) { return x * x; } function func3(func1, func2) { return function() { return func2.call(this, func1.apply(this, arguments)); } } var f3 = func3(f1, f2); console.log(f3(2, 3)); // 25
我们通过函数func3
将函数f1
,f2
结合到了一起,然后返回了一个新的函数f3
;这个函数就是我们期望的那个函数.
不完全函数(partial function)
什么是不完全函数呢?所谓的不完全函数和我们上面所说的柯里化基本差不多;所谓的不完全函数,就是给你想要运行的那个函数绑定一个固定的参数值;
然后后面的运行或者说传递参数都是在前面的基础上进行运行的.看下面的例子:
// 一个将函数的arguments对象变成一个数组的方法 function array(a, n) { return Array.prototype.slice.call(a, n || 0); } // 我们要运行的函数 function showMsg(a, b, c){ return a * (b - c); } function partialLeft(f) { var args = arguments; return function() { var a = array(args, 1); a = a.concat(array(arguments)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } function partialRight(f) { var args = arguments; return function() { var a = array(arguments); a = a.concat(array(args, 1)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } function partial(f) { var args = arguments; return function() { var a = array(args, 1); var i = 0; j = 0; for(; i < a.length; i++) { if(a[i] === undefined) { a[i] = arguments[j++]; } } a = a.concat(array(arguments, j)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } partialLeft(showMsg, 1)(2, 3); // 实际参数列表: [1, 2, 3] 所以结果是 1 * (2 - 3) = -1 partialRight(showMsg, 1)(2, 3); // 实际参数列表: [2, 3, 1] 所以结果是 2 * (3 - 1) = 4 partial(showMsg, undefined, 1)(2, 3); // 实际参数列表: [2, 1, 3] 所以结果是 2 * (1 - 3) = -4
JavaScript的柯里化与JavaScript的函数式编程密不可分,下面列举了一些关于JavaScript函数式编程的库,大家可以看一下:
推荐教程:《JS教程》
위 내용은 JavaScript 함수의 커링을 이해하고 마스터할 수 있도록 안내합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!