カリー化という言葉を初めて見たのは、アルゴリズム関連のブログを読んでいてカリー化関数について言及されていたときで、その時はとても高級な言葉だと感じました。実際には、これは高階関数の特別な使用法です。
やはり、どんな機能であっても、使いやすい名前がついていないと使い物になりません。
まず、カリー化とは一体何なのか見てみましょう。
ウィキペディアによると:カリーイング、英語:Currying(英訳すると本当に既視感に満ちている)とは、複数のパラメータを受け取る関数を、単一のパラメータ(元の関数の最初のパラメータ)を受け入れる関数に変換し、残りのパラメータを受け入れ、結果を返す新しい関数。
この説明は少し抽象的ですので、何度も実証されている add 関数を使って簡単な実装をしてみましょう。
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
実際、add 関数の x と y の 2 つのパラメーターは、関数を使用して x を受け取り、次に y パラメーターを処理する関数を返すように変更されます。ここで、アイデアはより明確になります。つまり、パラメータの一部だけを渡して関数を呼び出し、残りのパラメータを処理する関数を返すようにするということです。
しかし、これをカプセル化するのに多大な労力を費やすことに何の意味があるのか、という疑問が生じます。私たちプログラマーにとって、これ以上、何の利益も得られないことを行うことは、この世であっても不可能です。
カリー化の利点を挙げてみましょう?
1.パラメータの再利用
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
上記の例は通常のチェックです。通常は直接チェック関数を呼び出すだけですが、多くの場所に数字があるかどうかをチェックする必要がある場合は、実際には最初のパラメータ reg を再利用する必要があるため、hasNumber を直接呼び出すことができます。 hasLetter やその他の関数を使用すると、パラメーターを再利用でき、呼び出しがより便利になります。
2. 事前確認
var on = function(element, event, handler) {
if (document.addEventListener) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
} else {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}
var on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent('on' + event, handler);
}
}
プロジェクトに取り組んでいるとき、いくつかの DOM 操作をカプセル化することは非常に一般的です。上記の最初の記述方法も比較的一般的ですが、2 番目の記述方法を見てみましょう。それは比較的異なります。最初の記述方法自己実行して新しい関数を返すことですが、これにより、毎回判断することを避けるために、どのメソッドが実行されるかが事前に決定されます。
3. 遅延動作
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
私たちのjsでよく使われるbindと同様に、実装メカニズムはカリー化です。
これらの利点について話した後、まだ問題があることがわかりました。Currying を使用するたびに基になる関数を変更する必要があるのでしょうか?
一般的な梱包方法はありますか?
// 初步封装
var currying = function(fn) {
// args 获取第一个方法内的全部参数
var args = Array.prototype.slice.call(arguments, 1)
return function() {
// 将后面方法里的全部参数和args进行合并
var newArgs = args.concat(Array.prototype.slice.call(arguments))
// 把合并后的参数通过apply作为fn的参数并执行
return fn.apply(this, newArgs)
}
}
ここでは、最初に予備的なカプセル化を行い、クロージャを通じて予備的なパラメータを保存し、次にスプライシング用の残りの引数を取得し、最後にカリー化が必要な関数を実行します。
ただし、いくつか欠陥があるようです。このように返すと、あと 1 つのパラメータしか展開できません。Currying(a)(b)(c) はサポートされていないようです (複数パラメータの呼び出しはサポートされていません)。一般的に、これは、再帰を使用して別のレイヤーをカプセル化します。
// 支持多参数传递
function progressCurrying(fn, args) {
var _this = this
var len = fn.length;
var args = args || [];
return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}
// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}
これは実際には予備的なものであり、再帰呼び出しが追加されており、パラメータの数が最初の fn.length よりも小さい限り、再帰は実行され続けます。
利点についての説明が終わり、一般的な方法が利用可能になったので、カレーのパフォーマンスに焦点を当てましょう。
カレーのパフォーマンスの問題については、次の 4 つの点を知っておくだけで済みます。
- 一般に、引数オブジェクトへのアクセスは、名前付きパラメータへのアクセスよりも遅くなります。
- 一部の古いブラウザは、arguments.length の実装が非常に遅いです。
- fn.apply(…) と fn.call(…) を使用すると、通常は fn(…) を直接呼び出すよりも若干遅くなります。
- ネストされたスコープとクロージャを多数作成すると、メモリと速度の両方の面でコストがかかります。
実際、ほとんどのアプリケーションでは、主なパフォーマンスのボトルネックは DOM ノードの操作にありますが、js のパフォーマンスの低下は基本的に無視できるため、curry を直接安全に使用できます。
最後に、典型的な面接の質問を詳しく説明します
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9