私たちは、真の関数型プログラミングを実行できるように JavaScript を調整する方法を探しています。そのためには、関数呼び出しと関数プロトタイプを詳しく理解する必要があります。
関数プロトタイプ
さて、上にリンクされた記事を読んでも無視しても、次に進む準備は完了です。
お気に入りのブラウザ + JavaScript コンソールをクリックしたら、Function.prototype オブジェクトのプロパティを見てみましょう:
; html-script: false ] Object.getOwnPropertyNames(Function.prototype) //=> ["length", "name", "arguments", "caller", // "constructor", "bind", "toString", "call", "apply"]
ここでの出力は、使用しているブラウザと JavaScript のバージョンによって異なります。 (Chrome 33 を使用しています)
興味のあるプロパティがいくつか表示されます。この記事では、次について説明します。
Function.prototype.length
Function.prototype.call
Function.prototype.apply
最初のものはプロパティで、他の 2 つはメソッドです。これら 3 つに加えて、(非推奨になった) Function.prototype.arguments とは少し異なるこの特殊な変数引数についても説明したいと思います。
まず、何が起こっているのかを把握するために「テスター」関数を定義します。
; html-script: false ] var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c }); };
この関数は、入力パラメータの値とその値である「コンテキスト変数」を記録するだけです。
さて、何か試してみましょう:
; html-script: false ] tester("a"); //=> {this: Window, a: "a", b: (undefined), c: (undefined)} tester("this", "is", "cool"); //=> {this: Window, a: "this", b: "is", c: "cool"}
2 番目と 3 番目のパラメータを入力しないと、プログラムはそれらを未定義として表示することに気付きました。さらに、この関数のデフォルトの「コンテキスト」がグローバル オブジェクト ウィンドウであることに注意してください。
Function.prototype.call を使用する
関数の .call メソッドは、コンテキスト変数 this を最初の入力パラメーターの値に設定し、他のパラメーターを 1 つずつ渡す方法でこの関数を呼び出します。関数を入力してください。
構文:
; html-script: false ] fn.call(thisArg[, arg1[, arg2[, ...]]])
したがって、次の 2 行は同等です:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool");
もちろん、必要に応じて任意のパラメーターを渡すことができます:
; html-script: false ] tester.call("this?", "is", "even", "cooler"); //=> {this: "this?", a: "is", b: "even", c: "cooler"}
このメソッドの主な機能は、関数のこの変数の値を設定することです。あなたが呼ぶ 。
Function.prototype.applyを使用する
関数の.applyメソッドは、.callよりも実用的です。 .call と同様に、.apply も、コンテキスト変数 this を入力パラメーター シーケンスの最初のパラメーターの値に設定することによって呼び出されます。入力パラメーター シーケンスの 2 番目と最後のパラメーターは、配列 (または配列のようなオブジェクト) として渡されます。
構文:
; html-script: false ] fun.apply(thisArg, [argsArray])
したがって、次の 3 行はすべて同等です:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); tester.apply(window, ["this", "is", "cool"]);
パラメーター リストを配列として指定できることは、ほとんどの場合非常に便利です (そうすることの利点は後でわかります)。
たとえば、Math.max は可変個引数関数です (関数は任意の数のパラメーターを受け入れることができます)。
; html-script: false ] Math.max(1,3,2); //=> 3 Math.max(2,1); //=> 2
それでは、数値の配列があり、Math.max 関数を使用して最大のものを見つける必要がある場合、1 行のコードでこれを行うにはどうすればよいでしょうか?
; html-script: false ] var numbers = [3, 8, 7, 3, 1]; Math.max.apply(null, numbers); //=> 8
.apply メソッドは、特別な引数変数、つまり引数オブジェクト
と組み合わせると、その重要性が実際に分かり始めます。
すべての関数式には、そのスコープで使用できる特別なローカル変数、つまり引数があります。そのプロパティを調べるために、別のテスター関数を作成しましょう:
; html-script: false ] var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments)); };
注: この場合、引数には列挙可能としてマークされていないいくつかのプロパティがあるため、上記のように Object.getOwnPropertyNames を使用する必要があります。そのため、コンソールを使用するだけです。 log(arguments) この方法では、それらは表示されません。
ここで、古い方法に従い、テスター関数を呼び出してテストします。
; html-script: false ] tester("a", "b", "c"); //=> ["0", "1", "2", "length", "callee"] tester.apply(null, ["a"]); //=> ["0", "length", "callee"]
arguments 変数の属性には、関数に渡される各パラメーターに対応する属性が含まれます。これらは、.length 属性および .callee 属性と何ら変わりません。 。
.callee 属性は、現在の関数を呼び出した関数への参照を提供しますが、これはすべてのブラウザーでサポートされているわけではありません。今のところ、このプロパティは無視します。
テスター関数を再定義して、もう少し機能を充実させましょう:
; html-script: false ] var tester = function() { console.log({ 'this': this, 'arguments': arguments, 'length': arguments.length }); }; tester.apply(null, ["a", "b", "c"]); //=> { this: null, arguments: { 0: "a", 1: "b", 2: "c" }, length: 3 }
引数: それはオブジェクトですか、それとも配列ですか?
arguments は多かれ少なかれ似ていますが、まったく配列ではないことがわかります。多くの場合、配列ではない場合でも配列として扱いたいと思うでしょう。引数を配列に変換する非常に優れたショートカット関数があります:
; html-script: false ] function toArray(args) { return Array.prototype.slice.call(args); } var example = function(){ console.log(arguments); console.log(toArray(arguments)); }; example("a", "b", "c"); //=> { 0: "a", 1: "b", 2: "c" } //=> ["a", "b", "c"]
ここでは Array.prototype.slice メソッドを使用して、配列のようなオブジェクトを配列に変換します。このため、引数オブジェクトは .apply と組み合わせて使用すると非常に便利になります。
いくつかの役立つ例
Log Wrapper
は logWrapper 関数を構築しますが、それは単項関数でのみ正しく動作します。
; html-script: false ] // old version var logWrapper = function (f) { return function (a) { console.log('calling "' + f.name + '" with argument "' + a); return f(a); }; };
もちろん、私たちの既存の知識を使用して、任意の関数を提供できる logWrapper 関数を構築することができます:
; html-script: false ] // new version var logWrapper = function (f) { return function () { console.log('calling "' + f.name + '"', arguments); return f.apply(this, arguments); }; };
; html-script: false ] f.apply(this, arguments);
我们确定这个函数f会在和它之前完全相同的上下文中被调用。于是,如果我们愿意用新的”wrapped”版本替换掉我们的代码中的那些日志记录函数是完全理所当然没有唐突感的。 把原生的prototype方法放到公共函数库中 浏览器有大量超有用的方法我们可以“借用”到我们的代码里。方法常常把this变量作为“data”来处理。在函数式编程,我们没有this变量,但是我们无论如何要使用函数的!
; html-script: false ] var demethodize = function(fn){ return function(){ var args = [].slice.call(arguments, 1); return fn.apply(arguments[0], args); }; };
; html-script: false ] // String.prototype var split = demethodize(String.prototype.split); var slice = demethodize(String.prototype.slice); var indexOfStr = demethodize(String.prototype.indexOf); var toLowerCase = demethodize(String.prototype.toLowerCase); // Array.prototype var join = demethodize(Array.prototype.join); var forEach = demethodize(Array.prototype.forEach); var map = demethodize(Array.prototype.map);
; html-script: false ] ("abc,def").split(","); //=> ["abc","def"] split("abc,def", ","); //=> ["abc","def"] ["a","b","c"].join(" "); //=> "a b c" join(["a","b","c"], " "); // => "a b c"
在函数式编程情况下,你通常需要把“data”或“input data”参数作为函数的最右边的参数。方法通常会把this变量绑定到“data”参数上。举个例子,String.prototype方法通常操作的是实际的字符串(即”data”)。Array方法也是这样。
为什么这样可能不会马上被理解,但是一旦你使用柯里化或是组合函数来表达更丰富的逻辑的时候情况会这样。这正是我在引言部分说到UnderScore.js所存在的问题,之后在以后的文章中还会详细介绍。几乎每个Underscore.js的函数都会有“data”参数,并且作为最左参数。这最终导致非常难重用,代码也很难阅读或者是分析。:-(
管理参数顺序
; html-script: false ] // shift the parameters of a function by one var ignoreFirstArg = function (f) { return function(){ var args = [].slice.call(arguments,1); return f.apply(this, args); }; }; // reverse the order that a function accepts arguments var reverseArgs = function (f) { return function(){ return f.apply(this, toArray(arguments).reverse()); }; };
组合函数
在函数式编程世界里组合函数到一起是极其重要的。通常的想法是创建小的、可测试的函数来表现一个“单元逻辑”,这些可以组装到一个更大的可以做更复杂工作的“结构”
; html-script: false ] // compose(f1, f2, f3..., fn)(args) == f1(f2(f3(...(fn(args...))))) var compose = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = length; // we need to go in reverse order while ( --i >= 0 ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; }; }; // sequence(f1, f2, f3..., fn)(args...) == fn(...(f3(f2(f1(args...))))) var sequence = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = 0; // we need to go in normal order here while ( i++ < length ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; }; };
例子:
; html-script: false ] // abs(x) = Sqrt(x^2) var abs = compose(sqrt, square); abs(-2); // 2