我們正在尋求調校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
第一個是個屬性,另外兩個是方法。除了這三個,我還會願意討論下這個特殊的變數arguments,它和Function.prototype.arguments(已被棄用)稍有不同。
首先,我將定義一個「tester」函數來幫助我們弄清楚發生了什麼事。
; html-script: false ] var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c }); };
這個函數簡單記錄了輸入參數的值,和“上下文變數”,即this的值。
現在,讓我們嘗試一些事情:
; 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個參數,程式將會顯示它們為undefined(未定義)。此外,我們注意到這個函數預設的「上下文」是全域物件window。
使用Function.prototype.call
一個函數的.call 方法以這樣的方式呼叫這個函數,它把上下文變數this設定為第一個輸入參數的值,然後其他的的參數一個跟一個的也傳進函數。
語法:
; html-script: false ] fn.call(thisArg[, arg1[, arg2[, ...]]])
因此,下面這兩行是等效的:
; 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"}
這個方法主要的功能是設定你所調用函數的this變數的值。
使用Function.prototype.apply
函數的.apply方法比.call更實用一些。和.call類似,.apply的呼叫方式也是把上下文變數this設定為輸入參數序列中的第一個參數的值。輸入參數序列的第二個參數也是最後一個,以數組(或類別數組物件)的方式傳入。
語法:
; html-script: false ] fun.apply(thisArg, [argsArray])
因此,下面三行全部等效:
; 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函數找出其中最大的那個,我怎麼用一行程式碼來做這個事兒呢?
; html-script: false ] var numbers = [3, 8, 7, 3, 1]; Math.max.apply(null, numbers); //=> 8
The .apply method really starts to show it’s importance when coupled with the special arguments variable: The arguments object
.apply方法真正開始顯示出它的重要是當配上特殊參數:Arguments。
每個函數表達式在它的作用域中都有一個特殊的、可使用的局部變數:arguments。為了研究它的屬性,讓我們建立另一個tester函數:
; html-script: false ] var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments)); };
註:在這種情況下我們必須像上面這樣使用Object.getOwnPropertyNames,因為arguments有一些屬性沒有標記為可以被枚舉的,於是如果僅使用console.log(arguments)這種方式它們將不會被顯示出來。
現在我們按照老辦法,透過呼叫tester函數來測試下:
; html-script: false ] tester("a", "b", "c"); //=> ["0", "1", "2", "length", "callee"] tester.apply(null, ["a"]); //=> ["0", "length", "callee"]
arguments變數的屬性中包含了對應於傳入函數的每個參數的屬性,這些和.length屬性、.callee屬性沒什麼不同。
.callee屬性提供了呼叫目前函數的函數的引用,但是這並不被所有的瀏覽器支援。就目前而言,我們忽略這個屬性。
讓我們重新定義一下我們的tester函數,讓它豐富一點:
; 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:是物件還是陣列?
我們看得出,arguments完全不是一個數組,雖然多多少少有點像。在很多情況下,儘管不是,我們還是希望把它當作數組來處理。把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同時使用的時候arguments物件最終會極為有用。
一些有用例子
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"
題外話:
後面我們會演示,實際上更好的使用demethodize函數的方式是參數翻轉。
在函数式编程情况下,你通常需要把“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