我們正在尋求調校JavaScript的方式,使得我們可以做些真正的函數語言程式設計。為了做到這一點,詳細理解函式呼叫和函式原型是非常有必要的。
這是一個(待定)關於使用JavaScript進行函數語言程式設計系列的第三篇文章。如果你是剛剛加入的,你可以跳回去看看之前的文章。
- 第一部分:引言
- 第二部分:如何打造“函式式”程式語言
函式原型
現在,不管你是已經讀了還是忽略掉上面的連結所對應的文章,我們準備繼續前進!
如果我們點開了我們喜歡的瀏覽器+JavaScript控制檯,讓我們看一下Function.prototype物件的屬性:
1 2 3 4 |
; 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”函式來幫助我們弄清楚發生了什麼。
1 2 3 4 5 6 7 8 9 |
; html-script: false ] var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c }); }; |
這個函式簡單記錄了輸入引數的值,和“上下文變數”,即this的值。
現在,讓我們嘗試一些事情:
1 2 3 4 5 6 |
; 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設定為第一個輸入引數的值,然後其他的的引數一個跟一個的也傳進函式。
語法:
1 2 |
; html-script: false ] fn.call(thisArg[, arg1[, arg2[, ...]]]) |
因此,下面這兩行是等效的:
1 2 3 |
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); |
當然,我們能夠隨需傳入任何引數:
1 2 3 |
; 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設定為輸入引數序列中的第一個引數的值。輸入引數序列的第二個引數也是最後一個,以陣列(或者類陣列物件)的方式傳入。
語法:
1 2 |
; html-script: false ] fun.apply(thisArg, [argsArray]) |
因此,下面三行全部等效:
1 2 3 4 |
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); tester.apply(window, ["this", "is", "cool"]); |
能夠以陣列的方式指定一個引數列表在多數時候非常有用(我們會發現這樣做的好處的)。
例如,Math.max是一個可變引數函式(一個函式可以接受任意數目的引數)。
1 2 3 4 5 6 |
; html-script: false ] Math.max(1,3,2); //=> 3 Math.max(2,1); //=> 2 |
這樣,如果我有一個數值陣列,並且我需要利用Math.max函式找出其中最大的那個,我怎麼用一行程式碼來做這個事兒呢?
1 2 3 4 |
; 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函式:
1 2 3 4 |
; html-script: false ] var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments)); }; |
注:在這種情況下我們必須像上面這樣使用Object.getOwnPropertyNames,因為arguments有一些屬性沒有標記為可以被列舉的,於是如果僅僅使用console.log(arguments)這種方式它們將不會被顯示出來。
現在我們按照老辦法,通過呼叫tester函式來測試下:
1 2 3 4 5 6 |
; html-script: false ] tester("a", "b", "c"); //=> ["0", "1", "2", "length", "callee"] tester.apply(null, ["a"]); //=> ["0", "length", "callee"] |
arguments變數的屬性中包括了對應於傳入函式的每個引數的屬性,這些和.length屬性、.callee屬性沒什麼不同。
.callee屬性提供了呼叫當前函式的函式的引用,但是這並不被所有的瀏覽器支援。就目前而言,我們忽略這個屬性。
讓我們重新定義一下我們的tester函式,讓它豐富一點:
1 2 3 4 5 6 7 8 9 10 11 |
; 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轉換成一個陣列,這有個非常不錯的快捷小函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
; 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函式,但是它只是在一元函式下正確工作。
1 2 3 4 5 6 7 8 |
; html-script: false ] // old version var logWrapper = function (f) { return function (a) { console.log('calling "' + f.name + '" with argument "' + a); return f(a); }; }; |
當然了,我們既有的知識讓我們能夠構建一個可以服務於任何函式的logWrapper函式:
1 2 3 4 5 6 7 8 |
; html-script: false ] // new version var logWrapper = function (f) { return function () { console.log('calling "' + f.name + '"', arguments); return f.apply(this, arguments); }; }; |
通過呼叫
1 2 |
; html-script: false ] f.apply(this, arguments); |
我們確定這個函式f會在和它之前完全相同的上下文中被呼叫。於是,如果我們願意用新的”wrapped”版本替換掉我們的程式碼中的那些日誌記錄函式是完全理所當然沒有唐突感的。
把原生的prototype方法放到公共函式庫中
瀏覽器有大量超有用的方法我們可以“借用”到我們的程式碼裡。方法常常把this變數作為“data”來處理。在函數語言程式設計,我們沒有this變數,但是我們無論如何要使用函式的!
1 2 3 4 5 6 7 |
; html-script: false ] var demethodize = function(fn){ return function(){ var args = [].slice.call(arguments, 1); return fn.apply(arguments[0], args); }; }; |
一些別的例子:
1 2 3 4 5 6 7 8 9 10 11 |
; 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); |
當然,許多許多。來看看這些是怎麼執行的:
1 2 3 4 5 6 7 8 9 10 11 12 |
; 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”引數,並且作為最左引數。這最終導致非常難重用,程式碼也很難閱讀或者是分析。:-(
管理引數順序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
; 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()); }; }; |
組合函式
在函數語言程式設計世界裡組合函式到一起是極其重要的。通常的想法是建立小的、可測試的函式來表現一個“單元邏輯”,這些可以組裝到一個更大的可以做更復雜工作的“結構”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
; 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]; }; }; |
例子:
1 2 3 4 5 |
; html-script: false ] // abs(x) = Sqrt(x^2) var abs = compose(sqrt, square); abs(-2); // 2 |
這就是今天要說的,在接下來的文章中我們會在函式柯里化上深入研究下。
接下來 -> 第四部分:函式柯里化
更多內容預告:
- 第一部分:引言
- 第二部分:如何打造“函式式”程式語言
- 第三部分:.apply()、.call()以及arguments物件
- 第四部分:函式柯里化
- 第五部分:引數可變函式(敬請期待)
- 第六部分:一個真實的例子——2048 Game & Solver(敬請期待)
- 第七部分:惰性序列 / 集合(敬請期待)
- 第八部分:引數順序為何重要(敬請期待)
- 第九部分:Functors 和 Monads(敬請期待)