JavaScript函式柯里化
一、定義:
柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。
透過一個簡單的例子解釋一下:
function add(a, b) {
return a + b
}
add(1, 2); // 3
將函式add轉化為柯里化函式_add:
function _add(a){
return function(b){
return a + b
}
}
_add(1)(2); // 3
函式add和函式_add是等價的。
_add能夠處理add的所有剩餘引數,因此柯里化也被稱為部分求值。
實際上就是把add函式的a,b兩個引數變成了先用一個函式接收a然後返回一個函式去處理b引數。
現在思路應該就比較清晰了:只傳遞給函式一部分引數來呼叫,讓它返回一個函式去處理剩下的引數。
二、柯里化函式的作用
1、引數複用
案例:拼接地址
按照普通思路去拼接一個地址
// 拼接地址
function getUrl(protocol, hostname, pathname) {
return `${protocol}${hostname}${pathname}`;
}
const url1 = getUrl('https://', 'www.baidu.com', '/hasa');
const url2 = getUrl('https://', 'www.zhihu.com', '/saandsa');
const url3 = getUrl('https://', 'www.segmentfault.com', '/hasak');
console.log(url1, url2, url3)
每次呼叫getUrl引數的時候都要重複的傳入引數'https://'。
柯里化封裝之後:
function curry(protocol) {
return function (hostname, pathname) {
return `${protocol}${hostname}${pathname}`;
}
}
const url_curry = curry('https://');
const url1 = url_curry('www.baidu.com', '/hasa');
const url2 = url_curry('www.zhihu.com', '/saandsa');
const url3 = url_curry('www.segmentfault.com', '/hasak');
console.log(url1, url2, url3)
很明顯,經過柯里化封裝之後,之後再進行地址拼接的時候,減少了引數個數,降低了程式碼重複率。
2、提前確認/提前返回
案例:相容IE瀏覽器事件的監聽方法(IE is dead)
傳統的方法:
/*
* @param element Object DOM元素物件
* @param type String 事件型別
* @param listener Function 事件處理函式
* @param useCapture Boolean 是否捕獲
*/
var addEvent = function (element, type, listener, useCapture) {
if (window.addEventListener) {
element.addEventListener(type, function (e) {
listener.call(element, e)
}, useCapture)
} else {
element.attachEvent('on' + type, function (e) {
listener.call(element, e)
})
}
}
缺陷就是,每次對DOM元素進行事件繫結時都需要重新進行判斷,其實對於事件監聽網頁一發布瀏覽器已經確定了,就可以知曉瀏覽器到底是需要哪一種監聽方式。所以我們可以讓判斷只執行一次。
柯里化封裝之後:
var addEvent = (function () {
if (window.addEventListener) {
return function (element, type, listener, useCapture) { // return funtion
element.addEventListener(type, function () {
listener.call(element)
}, useCapture)
}
} else {
return function (element, type, listener) {
element.attachEvent('on' + type, function () {
listener.call(element)
})
}
}
})()
addEvent(element, "click", listener)
立即執行函式,在觸發多次事件也依舊只會觸發一次if條件判斷。
3、延遲執行
案例:釣魚統計重量
傳統的方法:
let fishWeight = 0;
const addWeight = function(weight) {
fishWeight += weight;
};
addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
console.log(fishWeight);
每次執行addWeight方法時,都進行一次魚的體重的加和。
柯里化封裝後:
function curryWeight(fn) {
let _fishWeight = [];
return function () {
if (arguments.length === 0) {
return fn.apply(null, _fishWeight);
} else {
_fishWeight = _fishWeight.concat([...arguments]);
}
}
}
function addWeight() {
let fishWeight = 0;
for (let i = 0, len = arguments.length; i < len; i++) {
fishWeight += arguments[i];
}
return fishWeight;
}
const _addWeight = curryWeight(addWeight);
_addWeight(6.5);
_addWeight(1.2);
_addWeight(2.3);
_addWeight(2.5);
console.log(_addWeight())
在執行_addWeight方法時,並沒有做魚的體重的加和,之後在最後一次執行_addWeight()時,才做了加和。做到了延遲執行addWeight方法的效果。
-----擴充:對魚的體重進行排序-----
function curryWeight(fn) {
let _fishWeight = [];
return function () {
if (arguments.length === 0) {
return fn.apply(null, _fishWeight);
} else {
_fishWeight = _fishWeight.concat([...arguments]);
}
}
}
function sortWeight() {
return [...arguments].sort((a, b) => a - b);
}
const _addWeight = curryWeight(sortWeight);
_addWeight(6.5);
_addWeight(1.2);
_addWeight(2.3);
_addWeight(2.5);
console.log(_addWeight())
很簡單,只需要修改addWeight函式即可。
三、柯里化函式的實現
Done1:
step1:實現一個函式add(1)(2)
function add(num){
let sum = num;
return function(num){
return sum+num
}
}
console.log(add(1)(2));
step2:實現add(1)(2)(3)呢?
不能一直無限巢狀,所以,返回一個函式名,在函式內部判斷是否還有引數傳進來
function add(num){
let sum =num;
return adds = function (num1){
if(num1) {
sum = sum+num1;
return adds
}
return sum
}
}
console.log(add(1)(2)(3)());
step3:如果實現一個add(1,2,3)(2,1)(3)(2,1,3)呢?
因為現在每次呼叫傳入的引數是不定長的。所以並不能直接把形參給寫死。
function add() {
// 收集引數
let params = [...arguments];
const getParams = function () {
// 當傳入的引數為空時,對引數陣列遍歷求和。
if ([...arguments].length === 0) return params.reduce((pre, next) => pre + next, 0);
// 收集引數
params.push(...arguments);
return getParams;
}
// 重複呼叫,直到傳入引數為空
return getParams;
}
let a = add(1, 2)(2, 1)(3)();
console.log(a);
Done2:
function curry() {
let args = [...arguments];
let inner = function () {
args.push(...arguments);
return inner;
}
// 核心內容:隱式轉換,呼叫了內部的toString
inner.toString = function () {
return args.reduce(function (pre, next) {
return pre + next;
})
}
return inner;
}
const result = curry(1)(2);
console.log(+result);
有關隱式轉換
1、在JavaScript中,toString()方法和valueOf()方法都可以被改寫,如果被用以操作JavaScript解析器就會自動呼叫。所以在柯里化函式中最後重寫toString恰好能滿足在收集完所有引數後再去執行。
2、有關JavaScript隱式呼叫的帖子
四、柯里化總結
函式的柯里化,核心思想是收集引數,需要依賴引數以及遞迴,透過拆分引數的方式,來呼叫一個多引數的函式方法,以達到減少程式碼冗餘,增加可讀性的目的。
優點:
- 柯里化之後,我們沒有丟失任何引數:log 依然可以被正常呼叫;
- 我們可以輕鬆地生成偏函式,例如用於生成今天的日誌的偏函式;
- 入口單一;
- 易於測試和複用。
缺點
- 函式巢狀多;
- 佔記憶體,有可能導致記憶體洩漏(因為本質是配合閉包實現的);
- 效率差(因為使用遞迴);
- 變數存取慢,訪問性很差(因為使用了arguments);
應用場景:
- 減少重複傳遞不變的部分引數;
- 將柯里化後的callback引數傳遞給其他函式。