JavaScript 函數語言程式設計技巧 - 反柯里化

SHERlocked93發表於2019-05-19

作為函數語言程式設計語言,JS帶來了很多語言上的有趣特性,比如柯里化和反柯里化。

可以對照另外一篇介紹 JS 柯里化 的文章一起看~

1. 簡介

柯里化,是固定部分引數,返回一個接受剩餘引數的函式,也稱為部分計算函式,目的是為了縮小適用範圍,建立一個針對性更強的函式。核心思想是把多引數傳入的函式拆成單引數(或部分)函式,內部再返回撥用下一個單引數(或部分)函式,依次處理剩餘的引數。

反柯里化,從字面講,意義和用法跟函式柯里化相比正好相反,擴大適用範圍,建立一個應用範圍更廣的函式。使本來只有特定物件才適用的方法,擴充套件到更多的物件。

2. 實現

先來看看反柯里化的通用實現吧~

Function.prototype.unCurrying = function() {
  const self = this
  return function(...rest) {
    return Function.prototype.call.apply(self, rest)
  }
}
複製程式碼

解釋下:

  1. 為Function原型新增uncurrying方法,並在執行的時候儲存執行unCurrying的方法到self
  2. 借用apply把要借用的函式作為this環境賦給call,並傳入之後的形參作為引數執行

還有一個實現:

Function.prototype.unCurrying = function() {
  return this.call.bind(this)
}
複製程式碼

如果你覺得把函式放在Function.prototype上不太好,也可以這樣:

function unCurrying(fn) {
  return function(tar, ...argu) {
    return fn.apply(tar, argu)
  }
}
複製程式碼

3. 使用

3.1 簡單使用

unCurrying通用實現簡單的實用一下試試:

Function.prototype.unCurrying = function() {
  const self = this                        // 這裡的self就是Array.prototype.push方法
  return function(...rest) {              // rest為傳入的兩層引數[[1,2,3],4]
    return Function.prototype.call.apply(self, rest)
  }
}
const push = Array.prototype.push.unCurrying()

~function(...rest) {       // rest:[1,2,3]
  push(rest, 4)
  console.log(rest)    // [1, 2, 3, 4]
}(1, 2, 3)
複製程式碼

3.2 借用其他方法

反柯里化其實反映的是一種思想,即擴大方法的適用範圍,仍然呼叫剛剛的通用unCurrying方法借用push方法:

const push = Array.prototype.push.unCurrying()

const obj = { a: '嘻嘻' }
push(obj, '呵呵', '哈哈', '嘿嘿')
console.log(obj)                    // { '0': '呵呵', '1': '哈哈', '2': '嘿嘿', a: '嘻嘻', length: 3 }
複製程式碼

相當於obj.push(...),obj不僅多了類似於陣列一樣以數字作為索引的屬性,還多了個類似於陣列的length屬性,讓引擎自動管理陣列成員和length屬性;(文後有V8引擎實現push方法的原始碼) 這樣一個陣列的push方法就被借用出來,可以應用於任何其他物件了。

只要是方法,unCurrying就可以借用,call方法也可以:

var call = Function.prototype.call.unCurrying();
function $(id) {
    return this.getElementById(id);
}
call($, document, 'demo')            // #demo 元素
複製程式碼

相當於document.$('demo'),成功的借用了call方法,當然可以把document改成你希望作為this繫結到$的任何物件,比如{ getElementById:T=>console.log(T+'呃') } // demo呃

3.3 借用自己

unCurrying本身也是方法,也可以借用自己...-。-

const unCurrying = Function.prototype.unCurrying.unCurrying()
const map = unCurrying(Array.prototype.map)
map({ 0: 4, 1: 'a', 2: null, length: 3 }, n => n + n)                    // [8, "aa", 0]
複製程式碼

神奇吧~

4. 總結

簡單說,函式柯里化就是對高階函式的降階處理,縮小適用範圍,建立一個針對性更強的函式。舉栗子:

function(arg1,arg2)        // => function(arg1)(arg2)
function(arg1,arg2,arg3)        // => function(arg1)(arg2)(arg3)
function(arg1,arg2,arg3,arg4)        // => function(arg1)(arg2)(arg3)(arg4)
function(arg1,arg2,…,argn)        // => function(arg1)(arg2)…(argn)
複製程式碼

而反柯里化就是反過來,增加適用範圍,讓方法使用場景更大。使用unCurrying, 可以把原生方法借出來,讓任何物件擁有原生物件的方法。舉個栗子:

obj.func(arg1, arg2)        // => func(obj, arg1, arg2)
複製程式碼

也可以這樣理解: 柯里化是在運算前提前傳參,可以傳遞多個引數; 反柯里化是延遲傳參,在運算時把原來已經固定的引數或者this上下文等當作引數延遲到未來傳遞。


附:

V8引擎中Array.prototype.push方法原始碼實現:

function ArrayPush() {
    var n = TO_UINT32(this.length);
    var m = %_ArgumentsLength();
    for (var i = 0; i < m; i++) {
        this[i + n] = %_Arguments(i);        // 屬性拷貝
        this.length = n + m;                    // 修正length
        return this.length;
    }
}
複製程式碼

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

參考:

  1. JS 柯里化
  2. 前端開發者進階之函式反柯里化unCurrying
  3. JavaScript中有趣的反柯里化
  4. js柯里化適用場景,優缺點分別是什麼,還有個反柯里化?
  5. JS進階篇--JS中的反柯里化( uncurrying)

PS:歡迎大家關注我的公眾號【前端下午茶】,一起加油吧~

JavaScript 函數語言程式設計技巧 - 反柯里化

相關文章