1. 程式人生 > >《JavaScript 模式》讀書筆記(4)— 函式5

《JavaScript 模式》讀書筆記(4)— 函式5

  這一篇是函式部分的最後一篇。我們來聊聊Curry化。

十、Curry

  這部分我們主要討論Curry化和部分函式應用的內容。但是在深入討論之前,我們需要先了解一下函式應用的含義。

 

函式應用

  在一些純粹的函數語言程式設計語言中,函式並不描述為被呼叫(即called或invoked),而是描述為應用(applied)。在JavaScript中,我們可以做同樣的事情,使用方法Function.prototype.apply()來應用函式,這是由於JavaScript中的函式實際上是物件,並且它們還具有如下方法。

// 定義函式
var sayHi = function(who) {
    console.log("Hello" + (who? ", " + who : "") + "!");
};

// 呼叫函式
sayHi(); // 輸出"Hello"
sayHi('world'); // 輸出"Hello, world!"

// 應用函式
sayHi.apply(null, ["hello"]); // 輸出"Hello, hello!"

  正如上面的例子所看到的,呼叫(invoking)函式和應用(applying)函式可以得到完全相同的結果。apply()帶有兩個引數:第一個引數為將要繫結到該函式內部this的一個物件,而第二個引數是一個數組或多個引數變數,這些引數將變成可用於該函式內部的類似陣列的arguments物件。如果第一個引數為null(空),那麼this將指向全域性物件,此時得到的結果就恰好如同呼叫一個非指定物件時的方法。

  當函式是一個物件的方法時,此時不能傳遞null引用。這種情況下,這裡的物件將成為apply()的第一個引數:

// 定義函式
var alien= {
    sayHi: function(who) {
        console.log("Hello" + (who? ", " + who : "") + "!");
    }
} 
alien.sayHi('world'); // 輸出"Hello, world!"
sayHi.apply(alien, ["humans"]); // 輸出"Hello, humans!"

  在上面的程式碼中,sayHi()內部的this指向了alien物件。而在之前的例子中,this指向了全域性物件。

  正如上面的兩個例子所展示的那樣,這些都表明我們考慮的“呼叫函式”並不只是“句法糖(syntactic sugar)”,而是等價於函式應用。

  請注意,除了apply()以外,Function.prototype物件還有一個call()方法,但是這仍然只是建立在apply()之上的語法糖而已。有時候最好使用該語法糖:即當函式僅帶有一個引數時,可以根據實際情況避免建立只有一個元素的陣列的工作。

// 在這種情況下,第二種更有效率,節省了一個數組
sayHi.apply(alien,["humans"]);
sayHi.call(alien,"humans");

 

部分應用

  現在我們知道,呼叫函式實際上就是將一個引數集合應用到一個函式中,那有沒有可能只傳遞部分引數,而不是所有引數?這種情況就和手動處理一個數學函式所常採用的方法是相似的。假定有一個函式add()用以將兩個數字加在一起:x和y。下面的程式碼片段展示了給定x值為5,且y值為4的情況下的解決方案。

// 出於演示的目的
// 並不是合法的JavaScript
function add(x,y) {
    return x + y;
}
// 有以下函式
add(5,4);

// 第1步,替換一個引數
function add(5, y){
    return 5 + y;
}

// 第2步,替換其他引數
function add(5, 4) {
    return 5 + 4;
}

  再提醒一遍,第1、2步的程式碼是不合法的,僅演示目的。

  上面的程式碼段演示瞭如何手工解決部分函式應用的問題。可以獲取第一個引數的值,並且在整個函式中用已知的值5替代未知的x,然後重複同樣的步驟直至用完了所有的引數。

  對這個例子中的步驟1可以稱為部分應用(partial application),即我們金鷹用了第一個引數。當執行部分應用時,並不會獲得結果,相反會獲得另一個函式。

  下面的程式碼片段演示了家鄉的partialApply()方法的使用示例:

var add = function (x,y) {
    return x + y;
};

// 完全應用
add.apply(null,[5,4]); // 9

// 部分應用
var newadd = add.partialApply(null,[5]);
// 應用一個引數到新函式中
newadd.apply(null,[4]); // 9

  如上面的程式碼所示,部分應用向我們提供了另一個新函式,隨後再以其他引數呼叫該函式。這種執行方式實際上與add(5)(4)有一些類似,這是由於add(5)返回了一個可在後來用(4)來呼叫的函式。

  此外,我們所熟悉的add(5, 4)呼叫方式可能並不像是“句法糖(syntactic sugar)”,相反,使用add(5)(4)才像是“句法糖(syntactic sugar)”。

  現在,返回到現實,JavaScript中並沒有partialApply()方法和函式,預設情況下也並不會出現與上面類似的行為。但是可以構造出這些函式,因為JavaScript的動態性足夠支援這種行為。

  使函式理解並處理部分應用的過程就成為Curry過程(Currying)。

 

Curry化

  這裡的curry源於數學家Haskell Curry的名字。Curry化是一個轉換過程,即我們執行函式轉換的過程。那麼,我們如何Curry化一個函式?其他的函式式語言可能已經將這種Curry化轉換構建到語言本身中,並且所有的函式已經預設轉換過,在JavaScript中,可以將add()函式修改成一個用於處理部分應用的Curry化函式。

  下面,我們來看個例子:

// curry化的add()函式
// 接受部分引數列表
function add(x,y) {
    var oldx = x,oldy = y;
    if(typeof oldy === 'undefined') { // 部分
        return function(newy) {
            return oldx + newy;
        };
    }
    // 完全應用
    return x + y;
}

// 測試
console.log(typeof add(5)); // 輸出“function”
add(3)(4); // 7
// 建立並存儲一個新函式
var add2000 = add(2000);
add2000(19); //輸出2010

  在上面的程式碼段中,當第一次呼叫add()時,它為返回的內部函式建立了一個閉包。該閉包將原始的x和y值儲存到私有變數oldx和oldy中。第一個私有變數oldx將在內部函式執行的時候使用。如果沒有部分應用,並且同時傳遞x和y值,該函式則繼續執行,並簡單將其相加。這種add()實現與實際需求相比顯得比較冗長,在這裡只是出於演示的目的這樣實現。下面將顯示一個更為精簡的實現版本。其中並沒有oldx和oldy,僅是因為原始x隱式的儲存在閉包中,並且還將y作為區域性變數複用,而不是像之前那樣建立一個新的變數newy:

// curry化的add()函式
// 接受部分引數列表
function add(x, y) {
    if(typeof y === 'undefined') { //部分
        return function(y) {
            return x + y;
        };
    }
    // 完全應用
    return x + y;
}

  在這些例子中,函式add()本身負責處理部分應用。但是能夠以更通用的方式執行相同給的任務麼?也就是說,是否可以將任意的函式轉換成一個新的可以接收部分引數的函式?

function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments,1);
    return function () {
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null,args);
    }
}

  schonfinkelize()函式可能不應該有這麼複雜,只是由於JavaScript中arguments並不是一個真實的陣列。從Array.prototype中借用slice()方法可以幫助我們將arguments變成一個數組,並且使用該陣列更加方便。當schonfinkelize()第一次呼叫時,它儲存了一個指向slice()方法的私有引用(名為slice),並且還儲存了呼叫該方法後的引數(存入stored_args中),該方法僅剝離了第一個引數,這是因為第一個引數是將被curry化的函式。然後,schonfinkelize()返回了一個新函式。當這個新函式被呼叫時,它訪問了已經私有儲存的引數stored_args以及slice引用。這個新函式必須將原有的部分應用引數(stored_args)合併到新引數(new_args),然後再將它們應用到原始函式fn中(也僅在閉包中私有可用)。

 

 

  我們來測試下上面的轉換方法:

function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments,1);
    return function () {
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null,args);
    }
}

// 普通函式
function add(x, y){
    return x + y;
}

// 將一個函式curry化並獲得一個新的函式
var newadd = schonfinkelize(add,5);
console.log(newadd(4)); //輸出9

// 另一種選擇,直接呼叫新函式
console.log(schonfinkelize(add,6)(7)); //輸出13

// 轉換函式並不侷限於單個引數或者單步Curry化
// 普通函式
function addSome(a, b, c, d, e) {
    return a + b + c + d + e;
}

// 可運行於任意數量的引數
console.log(schonfinkelize(addSome,1,2,3)(5,5));

// 兩步curry化
var addOne = schonfinkelize(addSome,1);
console.log(addOne(10,10,10,10)); //41
var addSix = schonfinkelize(addOne,2,3);
console.log(addSix(5,5)); // 16

  上面是完整的例子和測試。

  那什麼時候適合使用Curry化呢?當發現正在呼叫同一個函式,並且傳遞的引數絕大多數都是相同的,那麼該函式可能是用於Curry化的一個很好的候選引數。可以通過將一個函式集合部分應用到函式中,從而動態建立一個新函式。這個新函式將會儲存重複的引數(因此,不必每次都傳遞這些引數),並且還會使用預填充原始函式所期望的完整引數列表。

 

小結

  在JavaScript中,有關函式的部分是十分重要的,我們本系列文章相關的主要函式部分已經到此告一段落了。本篇討論了有關函式的背景和術語。學習了JavaScript中兩個重要的特徵。即:

  • 函式是第一類物件,可以作為帶有屬性和方法的值以及引數進行傳遞。
  • 函式提供了局部作用域,而其他打括號並不能提供這種區域性作用域(當然現在的let是可以的)。此外還需要記住的是,宣告的區域性變數可被提升到區域性作用域的頂部。

  建立函式的語法包括:

  • 1.  函式命名錶達式。
  • 2. 函式表示式(與上面的相同,但是缺少一個名字),通常也稱為匿名函式。
  • 3. 函式宣告,與其他語言中的函式的語法類似。

  在涵蓋了函式的背景和語法之後,我們學習了一些有用的模式:

  1、API模式,它們可以幫助您為函式提供更好且更整潔的介面:

    回撥模式:將函式作為引數進行傳遞。

    配置物件:有助於保持受到控制的函式的引數數量。

    返回函式:當一個函式的返回值是另一個函式時。

    Curry化:當新函式是基於現有函式,並加上部分引數列表建立時。

  2、初始化模式,它們可以幫助您在不汙染全域性名稱空間的情況下,使用臨時變數以一種更加整潔、結構化的方式執行初始化以及設定任務(當涉及web網頁和應用程式時是非常普遍的)。這些模式包括:

    即時函式:只要定義之後就立即執行。

    即時物件初始化:匿名物件組織了初始化任務,提供了可被立即呼叫的方法。

    初始化時分支:幫助分支程式碼在初始化程式碼執行過程中僅檢測一次,這與以後在程式生命週期內多次檢測相反。

  3、效能模式,可以幫助加速程式碼執行,這些模式包括:

    備忘模式:使用函式屬性以便使得計算過的值無須再次計算。

    自定義模式:以新的主體重寫本身,以使得在第二次或以後呼叫時僅需執行更少的工作。

 

  好了,函式部分到此結束了。我們下面會開始學習物件模式部分。加油!fighting!