カリー化は比較的高度な関数の応用であり、理解するのは簡単ではありません。なので、もっとわかりやすく表現するにはどうすればいいのか、ずっと考えていました。長い間考えた結果、カリー化という概念を脇に置いて、重要だが見落とされがちな 2 つの知識ポイントを追加することにしました。
1. 関数の暗黙的変換に関する補足知識
JavaScript は弱い型付け言語であり、その暗黙的変換は非常に柔軟で興味深いものです。暗黙的な変換を深く理解していない場合、4 + true = 5 などの一部の演算の結果に混乱する可能性があります。もちろん、暗黙的な変換を十分に理解していれば、js の使用能力を大幅に向上できることは間違いありません。ただ、暗黙的な変換ルールをすべて共有するつもりはありません。ここでは、関数の暗黙的な変換に関するいくつかのルールのみを共有します。
簡単な思考の質問をしてみましょう。
function fn() { return 20; } console.log(fn + 10); // 输出结果是多少?
いくつかの変更を加えて、出力がどのようになるかを考えてみませんか?
rreee引き続き変更できます。
function fn() { return 20; } fn.toString = function() { return 10; } console.log(fn + 10); // 输出结果是多少?
function fn() { return 20; } fn.toString = function() { return 10; } fn.valueOf = function() { return 5; } console.log(fn + 10); // 输出结果是多少?
console.log を使用するとき、または操作を実行するときに、暗黙的な変換が発生する可能性があります。上記の 3 つの例から、関数の暗黙的な変換についていくつかの結論を引き出すことができます。
toString と valueOf を再定義しない場合、関数の暗黙的な変換により、デフォルトの toString メソッドが呼び出され、関数の定義内容が文字列として返されます。 toString/vauleOf メソッドを積極的に定義すると、暗黙的な変換の戻り結果は自分自身で制御されます。 valueOf の優先順位は toString よりも高くなります。
つまり、上記の例の結論は理解しやすいです。試してみることをお勧めします。
2. 補足知識ポイント: call/apply を使用して配列をシールするマップメソッド
map(): 配列内の各項目に対して指定された関数を実行し、各関数を返す結果の配列を呼び出します。
平たく言えば、配列の各要素を走査し、マップの最初のパラメーター (コールバック関数) で計算を実行し、計算結果を返すことです。すべての計算結果から構成される新しい配列を返します。
// 输出结果分别为 function fn() { return 20; }10 20 15
は、上記の例のコメントで、map メソッドの詳細を詳しく説明しました。ここで、マップをどのようにカプセル化するかという問題に直面する必要があります。
まず for ループについて考えてみましょう。 for ループを使用してマップを実装できますが、カプセル化する際にはいくつかの問題を考慮します。 for ループを使うと、確かにループ処理をカプセル化するのは簡単ですが、for ループ内の各項目に対して何をするかが決まったものでカプセル化するのは困難です。すべてのシナリオで、for ループ内のデータの処理が明らかに異なるためです。
そこで、これらのさまざまな操作を別の関数で処理する良い方法をみんなで考え、この関数をマップ メソッドの最初のパラメーターにします。具体的には、このコールバック関数でどのような操作を行うかは私たち次第です。使用するときの判断に。したがって、この考え方に基づくカプセル化の実装は次のようになります。
// 回调函数中有三个参数 // 第一个参数表示newArr的每一项,第二个参数表示该项在数组中的索引值 // 第三个表示数组本身 // 除此之外,回调函数中的this,当map不存在第二参数时,this指向丢失,当存在第二个参数时,指向改参数所设定的对象 var newArr = [1, 2, 3, 4].map(function(item, i, arr) { console.log(item, i, arr, this); // 可运行试试看 return item + 1; // 每一项加1 }, { a: 1 }) console.log(newArr); // [2, 3, 4, 5]
上記のパッケージでは、最初に空の一時配列を定義しました。これは、最終的な戻り結果を格納するために使用されます。 for ループでは、ループするたびにパラメーター fn 関数が 1 回実行され、fn のパラメーターが call メソッドを使用して渡されます。
map のカプセル化プロセスを理解すると、map を使用するときに最初のコールバック関数で常に戻り値を期待する理由が理解できます。 eslintのルールではmapを使用する際に戻り値を設定しないとエラーと判断されます。
さて、関数の暗黙の変換ルールと、このシナリオでの call/apply の使用方法を理解したので、簡単な例を通してカリー化を理解してみましょう。
3. 簡単なものから奥深いものまでのカリー化
フロントエンドのインタビューでは、広く出回っているカリー化に関するインタビューの質問があります。
計算結果が次の期待を満たすことができるように add メソッドを実装します:
Array.prototype._map = function(fn, context) { var temp = []; if(typeof fn == 'function') { var k = 0; var len = this.length; // 封装for循环过程 for(; k < len; k++) { // 将每一项的运算操作丢进fn里,利用call方法指定fn的this指向与具体参数 temp.push(fn.call(context, this[k], k, this)) } } else { console.error('TypeError: '+ fn +' is not a function.'); } // 返回每一项运算结果组成的新数组 return temp; } var newArr = [1, 2, 3, 4].map(function(item) { return item + 1; }) // [2, 3, 4, 5]
明らかに、計算結果はすべてのパラメーターの合計であり、add メソッドが実行されるたびに同じ関数を返し、継続する必要があります。残りのパラメータを計算します。
最も単純な例から段階的に解決策を見つけることができます。
2回呼び出すだけの場合は、次のようにカプセル化できます。
add(1)(2)(3) = 6 add(1, 2, 3)(4) = 10 add(1)(2)(3)(4)(5) = 15
3 回のみ呼び出された場合:
function add(a) { return function(b) { return a + b; } } console.log(add(1)(2)); // 3
上記のカプセル化は、必要な結果に多少似ていますが、パラメーターの使用が非常に制限されているため、これは私たちが必要とする最終的な結果ではありません。 。どうすればいいですか?上記 2 つの例を要約すると、実際にはクロージャの特性を使用して、すべてのパラメータを最終的に返される関数に集中させ、計算して結果を返します。したがって、カプセル化する際の主な目的は、計算用のパラメータを集中させることです。
具体的な実装を見てみましょう。
function add() { // 第一次执行时,定义一个数组专门用来存储所有的参数 var _args = [].slice.call(arguments); // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值 var adder = function () { var _adder = function() { [].push.apply(_args, [].slice.call(arguments)); return _adder; }; // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } return adder.apply(null, [].slice.call(arguments)); } // 输出结果,可自由组合的参数 console.log(add(1, 2, 3, 4, 5)); // 15 console.log(add(1, 2, 3, 4)(5)); // 15 console.log(add(1)(2)(3)(4)(5)); // 15
上面的实现,利用闭包的特性,主要目的是想通过一些巧妙的方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。因此我们在调用add方法的时候,参数就显得非常灵活。当然,也就很轻松的满足了我们的需求。
那么读懂了上面的demo,然后我们再来看看柯里化的定义,相信大家就会更加容易理解了。
柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
1.接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。
2.将部分参数通过回调函数等方式传入函数中
3.返回一个新函数,用于处理所有的想要传入的参数
在上面的例子中,我们可以将add(1, 2, 3, 4)转换为add(1)(2)(3)(4)。这就是部分求值。每次传入的参数都只是我们想要传入的所有参数中的一部分。当然实际应用中,并不会常常这么复杂的去处理参数,很多时候也仅仅只是分成两部分而已。
咱们再来一起思考一个与柯里化相关的问题。
假如有一个计算要求,需要我们将数组里面的每一项用我们自己想要的字符给连起来。我们应该怎么做?想到使用join方法,就很简单。
var arr = [1, 2, 3, 4, 5]; // 实际开发中并不建议直接给Array扩展新的方法 // 只是用这种方式演示能够更加清晰一点 Array.prototype.merge = function(chars) { return this.join(chars); } var string = arr.merge('-') console.log(string); // 1-2-3-4-5
增加难度,将每一项加一个数后再连起来。那么这里就需要map来帮助我们对每一项进行特殊的运算处理,生成新的数组然后用字符连接起来了。实现如下:
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item + number; }).join(chars); } var string = arr.merge('-', 1); console.log(string); // 2-3-4-5-6
但是如果我们又想要让数组每一项都减去一个数组之后再连起来呢?当然和上面的加法操作一样的实现。
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item - number; }).join(chars); } var string = arr.merge('~', 1); console.log(string); // 0~1~2~3~4
机智的小伙伴肯定发现困惑所在了。我们期望封装一个函数,能同时处理不同的运算过程,但是我们并不能使用一个固定的套路将对每一项的操作都封装起来。于是问题就变成了和封装map的时候所面临的问题一样了。我们可以借助柯里化来搞定。
与map封装同样的道理,既然我们事先并不确定我们将要对每一项数据进行怎么样的处理,我只是知道我们需要将他们处理之后然后用字符连起来,所以不妨将处理内容保存在一个函数里。而仅仅固定封装连起来的这一部分需求。
于是我们就有了以下的封装。
// 封装很简单,一句话搞定 Array.prototype.merge = function(fn, chars) { return this.map(fn).join(chars); } var arr = [1, 2, 3, 4]; // 难点在于,在实际使用的时候,操作怎么来定义,利用闭包保存于传递num参数 var add = function(num) { return function(item) { return item + num; } } var red = function(num) { return function(item) { return item - num; } } // 每一项加2后合并 var res1 = arr.merge(add(2), '-'); // 每一项减2后合并 var res2 = arr.merge(red(1), '-'); // 也可以直接使用回调函数,每一项乘2后合并 var res3 = arr.merge((function(num) { return function(item) { return item * num } })(2), '-') console.log(res1); // 3-4-5-6 console.log(res2); // 0-1-2-3 console.log(res3); // 2-4-6-8
大家能从上面的例子,发现柯里化的特征吗?
四、柯里化通用式
通用的柯里化写法其实比我们上边封装的add方法要简单许多。
var currying = function(fn) { var args = [].slice.call(arguments, 1); return function() { // 主要还是收集所有需要的参数到一个数组中,便于统一计算 var _args = args.concat([].slice.call(arguments)); return fn.apply(null, _args); } } var sum = currying(function() { var args = [].slice.call(arguments); return args.reduce(function(a, b) { return a + b; }) }, 10) console.log(sum(20, 10)); // 40 console.log(sum(10, 5)); // 25
五、柯里化与bind
Object.prototype.bind = function(context) { var _this = this; var args = [].prototype.slice.call(arguments, 1); return function() { return _this.apply(context, args) } }
这个例子利用call与apply的灵活运用,实现了bind的功能。
在前面的几个例子中,我们可以总结一下柯里化的特点:
1.接收单一参数,将更多的参数通过回调函数来搞定?
2.返回一个新函数,用于处理所有的想要传入的参数;
3.需要利用call/apply与arguments对象收集参数;
4.返回的这个函数正是用来处理收集起来的参数。
希望大家读完之后都能够大概明白柯里化的概念,如果想要熟练使用它,就需要我们掌握更多的实际经验才行。