打造屬於本身的underscore系列(五)- 偏函數和函數柯里化

這一節的內容,主要針對javascript函數式編程的兩個重要概念,偏函數(partial application) 和函數柯里化(curry)進行介紹。着重講解underscore中對於偏函數應用的實現。javascript

四, 偏函數和函數柯里化

4.1 基本概念理解

javascript的函數式編程有兩個重要的概念,偏函數(partial application)和函數柯里化(curry)。理解這兩個概念以前,咱們須要先知曉什麼是函數式編程? 函數式編程是一種編程風格,它能夠將函數做爲參數傳遞,並返回沒有反作用的函數。而什麼是偏函數應用(partial application), 通俗點理解,固定一個函數的一個或者多個參數,也就是將一個 n 元函數轉換成一個 n - x 元函數;函數柯里化(curry)的理解,能夠歸納爲將一個多參數函數轉換成多個單參數函數,也就是將一個 n 元函數轉換成 n 個一元函數。舉兩個簡單的例子方便你們理解並對比其本質區別。java

// 偏函數
function add(a, b, c) {
    return a + b + c
}
var resultAdd = partial(add, 1); // partial 爲偏函數原理實現
resultAdd(2, 3) // 將多參數的函數轉化爲接收剩餘參數(n-1)的函數

// 函數柯里化
function add (a, b, c) {
    return a + b + c
}
var resultAdd = curry(add) //  curry 爲柯里化實現
resultAdd(1)(2)(3)  // 將多參數的函數轉化成接受單一參數的函數
複製代碼

在underscore中只有對偏函數應用的實現,並無函數柯里化的實現,所以本文只對underscore偏函數的實現作詳細探討,而柯里化實現只會在文末簡單說起。(tips: lodash 有針對curry的函數實現)es6

4.2 rest參數

偏函數和柯里化的實現依賴於reset參數的概念,這是一個ES6的概念,rest參數(...rest)用於獲取函數的多餘參數,好比;算法

function add (a, ...values) { console.log(values) } // [2,4,6]
add(1, 2, 4, 6) //  獲取除了第一個以後的剩餘參數並以數組的形式返回。
複製代碼

underscore中的restArguments方法,實現了與ES6中rest參數語法類似的功能,restArguments函數傳遞兩個參數,function 和起始reset的位置,返回一個function的版本,該版本函數在調用時會接收來自起始rest位置後的全部參數,並收集到一個數組中。若是起始rest位置沒有傳遞,則根據function自己的參數個數來肯定。因爲描述比較晦澀難懂,咱們能夠舉一個具體的例子編程

var result = function (a, b, c) {
    console.log(a) // 3
    console.log(b) // 15
    console.log(c) // [2, 3, 2]
    return 'haha'
}
var raceResults = _.restArguments(result);
raceResults(3,15,2,3,2)
複製代碼

result函數從接收三個參數,通過restArguments方法轉換後,將接收的多餘參數以數組的方式存儲。當傳遞起始reset位置即startIndex時,實例以下:數組

var result = function (a, b, c) {
    console.log(a) // 3
    console.log(b) // [15, 2, 3, 2]
    console.log(c) // undefined
    return ''
}
var raceResults = _.restArguments(result, 1);
raceResults(3,15,2,3,2)
複製代碼

startIndex 會指定原函數在何處將餘下的參數轉換成rest,例子中會在第一個參數以後將參數轉成rest數組形式。所以有了這兩種情景,咱們能夠實現一個簡化版的restArguments方法,具體的思路能夠參考代碼註釋bash

/**
 * 模仿es6 reset參數
 * fn  函數
 * [startIndex]: 接收參數的起始位置,如未傳遞,則爲fn自己參數個數
 */
_.restArguments = function (fn, startIndex) {
    return function () {
        var l = startIndex == null ? fn.length - 1 : startIndex; // 若是沒有傳遞startIndex,則rest數組的起始位置爲參數倒數第二個
        l = l - fn.length < 0 ? l : 0; // 若是startIndex有傳遞值,但該值超過函數的參數個數,則默認將rest數組的起始位置設爲第一個
        var arr = []
        var args = slice.call(arguments);
        for (var i = 0; i < l; i++) {
            arr.push(args[i]) // arr 存儲startIndex前的參數
        }
        var restArgs = slice.call(arguments, l)
        arr.push(restArgs) // 將startIndex後的參數以數組的形式插入arr中,eg: arr = [1,3,4,[2,5,6]]
        return fn.apply(this, arr) //  調用時,fn參數參數形式已經轉換成 1,3,4,[2,5,6]
    }
}
複製代碼

restArgument實現rest參數的形式,本質上是改變參數的傳遞方式,函數調用時會將指定位置後的參數轉化成數組形式的參數。app

4.3 不綁定this指向的偏函數應用

在4.1的偏函數概念理解中,咱們已經瞭解了偏函數的概念和使用形式,即將多參數的函數轉化爲接收剩餘參數(n-1)的函數。在underscore中_.partial方法提供了對偏函數的實現。框架

// 使用
_.partial(function, *arguments)
// 舉例
var subtract = function(a, b) { return b - a; };
sub5 = _.partial(subtract, 5);
sub5(20); // 15
// 能夠傳遞_ 給arguments列表來指定一個不預先填充,但在調用時提供的參數
subFrom20 = _.partial(subtract, _, 5);
subFrom20(20); // -15
複製代碼

有了restArguments的基礎,實現一個partial函數便水到渠成。調用partial時,函數通過restArguments這層包裝後,函數的剩餘參數直接轉成rest數組的形式,方便後續邏輯處理。函數式編程

/**
 * 偏函數
 * 不指定執行上下文
 */
_.partial = _.restArguments(function (fn, reset) { //  將後續參數轉化成rest數組形式
    return function () {
        var position = 0
        var placeholder = _.partial.placeholder; //  佔位符,預先不填充,調用時填充
        var length = reset.length;
        var args = Array(length);
        for (var i = 0; i < length; i++) {
            args[i] = reset[i] === placeholder ? arguments[position++] : reset[i]; // 預先存儲partial封裝時傳遞的參數,當遇到佔位符時,用partial處理後函數調用傳遞的參數代替。
        }
        while (position < arguments.length) {
            args.push(arguments[position++]) // 將partial處理後函數調用的參數和原存儲參數合併。真正調用函數時傳遞執行。
        }
        return fn.apply(this, args)
    }
})

_.partial.placeholder = _;
複製代碼

偏函數的思想,本質上能夠這樣理解,將參數保存起來,在調用函數時和調用傳遞參數合併,做爲真正執行函數時的參數。

4.4 綁定this指向的偏函數應用

_.partial方法雖然實現了偏函數,可是當方法的調用須要結合上下文時,patial方法沒法指定上下文,例如

var obj = {
    age: 1111,
    methods: function (name, time) {
        return name + '' + this.age + time 
    }
}

var sresult = _.partial(obj.methods, 3);
console.log(sresult(5)) // 3undefined5
複製代碼

從偏函數的定義咱們知道,原生javascript中,Function.prototype.bind()已經能夠知足偏函數應用了

function add3(a, b, c) { return a+b+c; }  
add3(2,4,8);  // 14

var add6 = add3.bind(this, 2, 4);  
add6(8);  // 14  
複製代碼

而在underscore一樣封裝了這樣的方法,_.bind(function, object, *arguments) , 從bind函數的定義中能夠知道,該方法將綁定函數 function 到對象 object 上, 也就是不管什麼時候調用函數, 函數裏的 this 都指向這個 object,而且能夠填充函數所須要的參數。它是一個能結合上下文的偏函數應用,所以只須要修改partial的調用方式便可實現bind方法。

/**
 * bind
 * 偏函數指定this
 */
_.bind = _.restArguments(function (fn, obj, reset) {
    return function () {
        var position = 0
        var placeholder = _.partial.placeholder;
        var length = reset.length;
        var args = Array(length);
        for (var i = 0; i < length; i++) {
            args[i] = reset[i] === placeholder ? arguments[position++] : reset[i]
        }
        while (position < arguments.length) {
            args.push(arguments[position++])
        }
        return fn.apply(obj, args) // 指定obj爲執行上下文
    }
})
複製代碼
4.5 其餘版本偏函數

至此,underscore中關於偏函數的實現已經介紹完畢,其設計思想是先將參數保存起來,在調用函數時和調用傳遞參數合併,做爲真正執行函數時的參數執行函數。所以拋離underscore,咱們能夠用arguments和es6的rest參數的方式來實現偏函數,下面提供兩個簡易版本。

// arguments版本
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)))
    }
}
// es6 rest版本
function partial(fn, ...rest) {
    return (...args) => {
     return fn(...rest, ...args)   
    }
}
複製代碼
4.6 函數柯里化

前文提到,underscore並無關於函數柯里化的實現,只在它的類似庫lodash纔有對柯里化的實現。柯里化的思想是將一個多參數的函數拆分爲接收單個參數的函數,接收單個參數的函數會返回另外一個函數,直到接收完全部參數後才返回計算結果。所以,實現思路能夠參考如下兩種,es6版本和前者的實現思路相同。

// 完整版柯里化 ES3
function curry(fn) {
    if(fn.length < 2) return fn; // 當fn的參數只有一個或者更少時, 直接返回該函數並不須要柯里化。
    const generate = function(args, length) {
        return !length ? fn.apply(this, args) : function(arg) {
            return generate(args.concat(arg), length -1) // 循環遞歸調用,直到接收完全部參數(與函數參數個數一致), 將全部參數傳遞給fn進行調用。
        }
    }
    return generate([], fn.length)
}
// 完整版柯里化es6
function curryEs6(fn) {
    if(fn.length < 2) return fn
    const generate = (args, length) => !length ? fn(...args) : arg => generate([...args, arg], length - 1);
    return generate([], fn.length)
}
複製代碼

柯里化的實現思路多樣,且衍生變種內容較多,這裏不一一闡述,有時間再另寫一篇深刻探討。而關於偏函數的應用,會有專門一節來介紹underscore中關於偏函數的應用,主要應用於延遲過程處理等。




相關文章
相關標籤/搜索