ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript 関数のカリー化を理解し、習得できるようにします。
Haskell と scala はどちらも関数のカリー化をサポートしています。JavaScript 関数のカリー化は JavaScript 関数プログラミングとも密接に関連しています。興味がある場合は、これらの側面をさらに理解して取り組むことができます。多くのことを得ることができると思います.
関数部分 #/ の call
/apply
##引数
コンピュータ サイエンスでは、カリー化 カリー化 (英語:Currying)は、カリー化またはカリー化とも訳され、複数のパラメータを受け入れる関数を単一のパラメータ(元の関数の最初のパラメータ)を受け入れる関数に変換し、残りのパラメータを受け入れる関数を返すことです。結果を返す新しい関数の場合。
簡単な例を挙げると、次の関数add はパラメータ
a と
b## を渡す一般的な関数です。 #Add; functioncurryingAdd
は function add
;をカリー化する関数です。このようにして、操作を実行するには 2 つのパラメーターを直接渡す必要があることがわかります。ここで、パラメータ
a
と b
をそれぞれ渡す必要があります。関数は次のとおりです: <pre class="brush:php;toolbar:false">function add(a, b) {
return a + b;
}
function curryingAdd(a) {
return function(b) {
return a + b;
}
}
add(1, 2); // 3
curryingAdd(1)(2); // 3</pre>
これを見た人は、何の役に立つのかと考えているかもしれません。これを行う理由は何ですか?これはアプリケーションにどのような利点をもたらしますか?心配しないで、読み進めましょう。
なぜ関数をカリー化する必要があるのですか?
いくつかのヒント (以下を参照) を使用できます。関数をカリー化する方法?
が含まれます。 、高階関数
、不完全関数
など
If関数を実装したいのですが、ステートメント
nameLikesong
を出力することです。このうち、name
と song
は可変パラメータです。 ; その後、一般的に次のように書きます: <pre class="brush:php;toolbar:false">function printInfo(name, song) {
console.log(name + '喜欢的歌曲是: ' + song);
}
printInfo('Tom', '七里香');
printInfo('Jerry', '雅俗共赏');</pre>
上記の関数をカリー化した後、次のように書くことができます:
function curryingPrintInfo(name) { return function(song) { console.log(name + '喜欢的歌曲是: ' + song); } } var tomLike = curryingPrintInfo('Tom'); tomLike('七里香'); var jerryLike = curryingPrintInfo('Jerry'); jerryLike('雅俗共赏');
カリー化で関数
printInfo をカリー化しましたが、カリー化が必要なときに上記のような関数をネストし続けることは望ましくありません。これは単なる悪夢です。したがって、他の関数のカリー化を支援する関数をいくつか作成する必要があります。当面は
curryingHelper
と呼びましょう。単純な curryingHelper
関数は次のとおりです: <pre class="brush:php;toolbar:false">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);
}
}</pre>
ここで少し説明します、まず、関数の
関数に渡されるパラメータ オブジェクトを表します。これは配列ではなく、配列のようなオブジェクトです。So関数の
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 は 高次関数#です##. 高階関数の説明については、以下を参照してください。
上記のカレーヘルパーこの関数は確かに私たちの一般的なニーズを満たすことができますが、それだけでは十分ではありません。カリー化した後でこれらの関数が機能することを願っています。一度に 1 つのパラメーターのみを渡すことができます。 後はパラメーターを複数回渡すことができます。では、どうすればよいでしょうか。もう少し頭を使って、
betterCurryingHelper 関数を作成して、上記の
関数で述べたことを実現できます。コードは次のとおりです: <pre class="brush:php;toolbar:false">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);
}
};
}</pre>
そのうちの
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セクションで説明されている関数です。これが確認できます
betterCurryingHelper は、私たちが望む関数を実現します。そして、元の関数も使用できます。その関数は、カレー関数も使用します。
我们已经能够写出很好的柯里化辅助函数了,但是这还不算是最刺激的,如果我们在传递参数的时候可以不按照顺来那一定很酷;当然我们也可以写出这样的函数来,
这个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 中国語 Web サイトの他の関連記事を参照してください。