博客地址:https://ainyi.com/74
定義
在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受余下的參數且返回結果的新函數的技術
就是只傳遞給函數某一部分參數來調用,返回一個新函數去處理剩下的參數(閉包)
常用的封裝成 add 函數
// reduce 方法
const add = (...args) => args.reduce((a, b) => a + b)
// 傳入多個參數,執行 add 函數
add(1, 2) // 3
// 假設有一個 currying 函數
let sum = currying(params)
sum(1)(3) // 4
實際應用
延遲計算
部分求和例子,說明了延遲計算的特點
const add = (...args) => args.reduce((a, b) => a + b)
// 簡化寫法
function currying(func) {
const args = []
return function result(...rest) {
if (rest.length === 0) {
return func(...args)
} else {
args.push(...rest)
return result
}
}
}
const sum = currying(add)
sum(1, 2)(3) // 未真正求值,收集參數的和
sum(4) // 未真正求值,收集參數的和
sum() // 輸出 10
上面的代碼理解:先定義 add 函數,然后 currying 函數就是用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數
上面的 currying 函數是一種簡化寫法,判斷傳入的參數長度是否為 0,若為 0 執行函數,否則收集參數到 args 數組
另一種常見的應用是 bind 函數,我們看下 bind 的使用
let obj = {
name: 'Krry'
}
const fun = function () {
console.log(this.name)
}.bind(obj)
fun() // Krry
這里 bind 用來改變函數執行時候的上下文this,但是函數本身並不執行,所以本質上是延遲計算,這一點和 call / apply 直接執行有所不同
動態創建函數
有一種典型的應用情景是這樣的,每次調用函數都需要進行一次判斷,但其實第一次判斷計算之后,后續調用並不需要再次判斷,這種情況下就非常適合使用柯里化方案來處理
即第一次判斷之后,動態創建一個新函數用於處理后續傳入的參數,並返回這個新函數。當然也可以使用惰性函數來處理,本例最后一個方案會介紹
我們看下面的這個例子,在 DOM 中添加事件時需要兼容現代瀏覽器和 IE 瀏覽器(IE < 9),方法就是對瀏覽器環境進行判斷,看瀏覽器是否支持,簡化寫法如下
// 簡化寫法
function addEvent (type, el, fn, capture = false) {
if (window.addEventListener) {
el.addEventListener(type, fn, capture);
}
else if(window.attachEvent) {
el.attachEvent('on' + type, fn);
}
}
但是這種寫法有一個問題,就是每次添加事件都會調用做一次判斷,比較麻煩
可以利用閉包和立即調用函數表達式(IIFE)來實現只判斷一次,后續都無需判斷
const addEvent = (function(){
if (window.addEventListener) {
return function (type, el, fn, capture) { // 關鍵
el.addEventListener(type, fn, capture)
}
}
else if(window.attachEvent) {
return function (type, el, fn) { // 關鍵
el.attachEvent('on' + type, fn)
}
}
})()
上面這種實現方案就是一種典型的柯里化應用,在第一次的 if...else if... 判斷之后完成第一次計算,然后動態創建返回新的函數用於處理后續傳入的參數
這樣做的好處就是之后調用之后就不需要再次調用計算了
當然可以使用惰性函數來實現這一功能,原理很簡單,就是重寫函數
function addEvent (type, el, fn, capture = false) {
// 重寫函數
if (window.addEventListener) {
addEvent = function (type, el, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else if(window.attachEvent) {
addEvent = function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
// 執行函數,有循環爆棧風險
addEvent(type, el, fn, capture);
}
第一次調用 addEvent 函數后,會進行一次環境判斷,在這之后 addEvent 函數被重寫,所以下次調用時就不會再次判斷環境
參數復用
我們知道調用 toString() 可以獲取每個對象的類型,但是不同對象的 toString() 有不同的實現
所以需要通過 Object.prototype.toString() 來獲取 Object 上的實現
同時以 call() / apply() 的形式來調用,並傳遞要檢查的對象作為第一個參數
例如下面這個例子
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
function isNumber(obj) {
return Object.prototype.toString.call(obj) === '[object Number]';
}
function isString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
// Test
isArray([1, 2, 3]) // true
isNumber(123) // true
isString('123') // true
但是上面方案有一個問題,那就是每種類型都需要定義一個方法,這里我們可以使用 bind 來擴展,優點是可以直接使用改造后的 toStr
const toStr = Function.prototype.call.bind(Object.prototype.toString);
// 改造前直接調用
[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
123.toString() // SyntaxError: Invalid or unexpected token
Object(123).toString() // "123"
// 改造后調用 toStr
toStr([1, 2, 3]) // "[object Array]"
toStr('123') // "[object String]"
toStr(123) // "[object Number]"
toStr(Object(123)) // "[object Number]"
上面例子首先使用 Function.prototype.call 函數指定一個 this 值,然后 .bind 返回一個新的函數,始終將 Object.prototype.toString 設置為傳入參數,其實等價於 Object.prototype.toString.call()
實現 Currying 函數
可以理解所謂的柯里化函數,就是封裝一系列的處理步驟,通過閉包將參數集中起來計算,最后再把需要處理的參數傳進去
實現原理就是用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數
上面延遲計算部分已經實現了一個簡化版的 Currying 函數
下面實現一個更加健壯的 Currying 函數
function currying(fn, length) {
// 第一次調用獲取函數 fn 參數的長度,后續調用獲取 fn 剩余參數的長度
length = length || fn.length
return function (...args) { // 返回一個新函數,接收參數為 ...args
// 新函數接收的參數長度是否大於等於 fn 剩余參數需要接收的長度
return args.length >= length
? fn.apply(this, args) // 滿足要求,執行 fn 函數,傳入新函數的參數
: currying(fn.bind(this, ...args), length - args.length)
// 不滿足要求,遞歸 currying 函數
// 新的 fn 為 bind 返回的新函數,新的 length 為 fn 剩余參數的長度
}
}
// Test
const fn = currying(function(a, b, c) {
console.log([a, b, c]);
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
上面使用的是 ES5 和 ES6 的混合語法
那如果不想使用 call/apply/bind 這些方法呢,自然是可以的,看下面的 ES6 極簡寫法,更加簡潔也更加易懂
const currying = fn =>
judge = (...args) =>
args.length >= fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg)
// Test
const fn = currying(function(a, b, c) {
console.log([a, b, c]);
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
如果還很難理解,看下面例子
function currying(fn, length) {
length = length || fn.length;
return function (...args) {
return args.length >= length
? fn.apply(this, args)
: currying(fn.bind(this, ...args), length - args.length)
}
}
const add = currying(function(a, b, c) {
console.log([a, b, c].reduce((a, b) => a + b))
})
add(1, 2, 3) // 6
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2, 3) // 6
擴展:函數參數 length
函數 currying 的實現中,使用了 fn.length 來表示函數參數的個數,那 fn.length 表示函數的所有參數個數嗎?並不是
函數的 length 屬性獲取的是形參的個數,但是形參的數量不包括剩余參數個數,而且僅包括第一個具有默認值之前的參數個數,看下面的例子
((a, b, c) => {}).length; // 3
((a, b, c = 3) => {}).length; // 2
((a, b = 2, c) => {}).length; // 1
((a = 1, b, c) => {}).length; // 0
((...args) => {}).length; // 0
const fn = (...args) => {
console.log(args.length);
}
fn(1, 2, 3) // 3
所以在柯里化的場景中,不建議使用 ES6 的函數參數默認值
const fn = currying((a = 1, b, c) => {
console.log([a, b, c])
})
fn() // [1, undefined, undefined]
fn()(2)(3) // Uncaught TypeError: fn(...) is not a function
我們期望函數 fn 輸出 1, 2, 3,但是實際上調用柯里化函數時 ((a = 1, b, c) => {}).length === 0
所以調用 fn() 時就已經執行並輸出了 1, undefined, undefined,而不是理想中的返回閉包函數
所以后續調用 fn()(2)(3) 將會報錯
小結&鏈接
定義:柯里化是一種將使用多個參數的函數轉換成一系列使用一個參數的函數,並且返回接受余下的參數而且返回結果的新函數的技術
實際應用
- 延遲計算:部分求和、bind 函數
- 動態創建函數:添加監聽 addEvent、惰性函數
- 參數復用:Function.prototype.call.bind(Object.prototype.toString)
實現 Currying 函數:用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數
函數參數 length:獲取的是形參的個數,但是形參的數量不包括剩余參數個數,而且僅包括第一個參數有默認值之前的參數個數
參考文章:JavaScript專題之函數柯里化
博客地址:https://ainyi.com/74