Js函式柯里化
函式柯里化
一、定義
在電腦科學中,柯里化(英語:Currying),指的是把“接受多個引數的函式”變換成“接受單一引數(最初函式的第一個引數) 的函式”,並返回“接受餘下的引數而且返回結果的新函式“的技術。
顧名思義,柯里化其實本身是固定一個可以預期的引數,並返回一個特定的函式,處理特定的需求。這增加了函式的適用性,但同時也降低了函式的通用性。
這樣的定義不太好理解,我們可以通過下面的例子配合解釋:
function A(a, b) {
return a + b
}
將函式A轉化為柯里化函式 _A:
function _A(a){
return function(b){
return a + b
}
}
那麼 _A 作為柯里化函式,他能夠處理A的剩餘引數。因此下面的執行結果是等價的。
A(1, 2);
_A(1)(2);
_A能夠處理A的所有剩餘引數,因此柯里化也被稱為部分求值。
實際上就是把A函式的a,b兩個引數變成了先用一個函式接收a然後返回一個函式去處理b引數。
現在思路應該就比較清晰了,只傳遞給函式一部分引數來呼叫,讓它返回一個函式去處理剩下的引數
二、柯里化的通用實現
柯里化,是函數語言程式設計的一個重要概念。它既能減少程式碼冗餘,也能增加可讀性。
現在看一個更復雜一點的例子,sum 是個簡單的累加函式,接受3個引數,輸出累加的結果:
function sum (a, b, c) {
console.log(a + b + c);
}
假設有這樣的需求,sum的前2個引數保持不變,最後一個引數可以隨意。那麼就會想到,在函式內,是否可以把前2個引數的相加過程,給抽離出來,因為引數都是相同的,沒必要每次都做運算。呼叫的寫法可以是這樣: sum(1, 2)(3)或sum(1, 2)(10),先把前2個引數的運算結果拿到後,再與第3個引數相加。這其實就是函式柯里化的簡單應用。
sum(1, 2)(3)這樣的寫法,並不常見。拆開來看sum(1, 2) 返回的應該還是個函式,因為後面還有 (3) 需要執行。那麼反過來,從最後一個引數,從右往左看,它的左側必然是一個函式。以此類推,如果前面有n個(),那就是有n個函式返回了結果,只是返回的結果還是一個函式。是不是有點遞迴的意思?
function curry (fn, currArgs) {
return function() {
let args = [].slice.call(arguments);
// 首次呼叫時,若未提供最後一個引數currArgs,則不用進行args的拼接
if (currArgs !== undefined) {
args = args.concat(currArgs);
}
// 遞迴呼叫
if (args.length < fn.length) {
return curry(fn, args);
}
// 遞迴出口
return fn.apply(null, args);
}
}
說明:
- curry有 2 個引數,fn 指的就是源處理函式 ;currArgs 是呼叫 curry 時傳入的引數列表
- 將 arguments 陣列化,arguments 是一個類陣列的結構,它並不是一個真的陣列,所以沒法使用陣列的方法。我們用了 call 的方法,就能愉快地對 args 使用陣列的原生方法了。
- args指的是當前柯里化的函式內已獲得的所有引數,這個函式最終會傳給fn
- 判斷 args 的個數,是否與 fn (也就是 sum )的引數個數相等,相等了就可以把引數都傳給 fn,進行輸出;否則,繼續遞迴呼叫,直到兩者相等。
三、柯里化的作用
1.引數複用
案例:正則驗證字串
按照普通的思路,驗證字串是否是正確的手機號或者郵箱
//驗證手機號
function checkPhone(phoneNumber) {
return /^1[34578]\d{9}$/.test(phoneNumber);
}
//驗證郵箱
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}
我們還可能會遇到驗證身份證號,驗證密碼等各種驗證資訊,因此在實踐中,為了統一邏輯,我們就會封裝一個更為通用的函式,將用於驗證的正則與將要被驗證的字串作為引數傳入
function check(reg,targetString) {
return reg.test(targetString);
}
check(/^1[34578]\d{9}$/, '14900000088');
check(/^1[34578]\d{9}$/, '14900000089');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
但是這樣封裝之後,在使用時又會稍微麻煩一點,因為會總是輸入一串正則,這樣就導致了使用時的效率低下。這個時候,我們就可以藉助柯里化,在check的基礎上再做一層封裝,以簡化使用。
var _check = curry(check)
var checkPhone = _check(/^1[34578]\d{9}$/)
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
checkPhone('183888888');
checkEmail('xxxxx@test.com');
2.提前確認
案例:相容IE瀏覽器事件的監聽方法
傳統的方法:
/*
* @param el Object DOM元素物件
* @param type String 事件型別
* @param fn Function 事件處理函式
* @param Capture Boolean 是否捕獲
*/
var addEvent = function(el, type, fn, capture) {
if(window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e)
}, capture)
} else {
el.attachEvent('on'+type, function(e) {
fn.call(el, e)
})
}
}
缺陷就是,每次對DOM元素進行事件繫結時都需要重新進行判斷,其實對於事件監聽網頁一發布瀏覽器已經確定了,就可以知曉瀏覽器到底是需要哪一種監聽方式。所以我們可以讓判斷只執行一次。
var curEvent = (function() {
if(window.addEventListener) {
return function(el, sType, fn, capture) { // return funtion
el.addEventListener(sType, function() {
fn.call(el)
}, capture)
}
} else {
return function(el, sType, fn) {
el.attachEvent('on'+sType, function() {
fn.call(el)
})
}
}
})
var addEvent = curEvent();
addEvent(el,"click",fn)
3.延遲執行
案例:釣魚統計重量
var fishWeight = 0;
var addWeight = function(weight) {
fishWeight += weight;
};
addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
console.log(fishWeight);
柯里化 :
function curryWeight(fn) {
var _fishWeight = [];
return function() {
if (arguments.length === 0) {
return fn.apply(null, _fishWeight);
} else {
_fishWeight = _fishWeight.concat([].slice.call(arguments));
}
}
}
function addWeight(){
var fishWeight = 0;
for (var i = 0, len = arguments.length; i<len; i++) {
fishWeight += arguments[i];
}
return fishWeight
}
var _addWeight = curryWeight(addWeight);
_addWeight(6.5)
_addWeight(1.2)
_addWeight(2.3)
_addWeight(2.5)
_addWeight()
四、柯里化總結
函式的柯里化,需要依賴引數以及遞迴,通過拆分引數的方式,來呼叫一個多引數的函式方法,以達到減少程式碼冗餘,增加可讀性的目的。
效能方面:
- 存取arguments物件通常要比存取命名引數要慢一點
- 一些老版本的瀏覽器在arguments.length的實現上是相當慢的
- 使用fn.apply( … ) 和 fn.call( … )通常比直接呼叫fn( … ) 稍微慢點
- 建立大量巢狀作用域和閉包函式會帶來花銷,無論是在記憶體還是速度上
應用場景:
- 減少重複傳遞不變的部分引數
- 將柯里化後的callback引數傳遞給其他函式
個人想法:
柯里化這個概念以及實現本身都非常難懂,平時寫程式碼幾乎也很少使用,能使用的場景真的不太多,大多數情況都選擇了其它簡單的方式實現了。在get到這個技能後,我認為以後可以在專案中適當使用這個方法,儘量減少重複程式碼。
五、經典面試題擴充套件
題目:實現一個add方法,使計算結果能夠滿足如下預期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
這個題目的目的是想讓add執行之後返回一個函式能夠繼續執行,最終運算的結果是所有出現過的引數之和。而這個題目的難點則在於引數的不固定。我們不知道函式會執行幾次。在此之前,補充2個非常重要的知識點。
-
不定參
// 將args陣列的子項展開作為add的引數傳入 function add(a, b, c, d) { return a + b + c + d; } var args = [1, 3, 100, 1]; //ES5 add.apply(null, args); //ES6 add(...args);
-
函式的隱式轉換
當我們直接將函式參與其他的計算時,函式會預設呼叫toString方法,直接將函式體轉換為字串參與計算。
function fn() { return 20 }
console.log(fn + 10) // 輸出結果 function fn() { return 20 }10
所以我們可以重寫函式的toString方法,讓函式參與計算時,輸出我們想要的結果。
function fn() { return 20 }
fn.toString = function() { return 30 }
console.log(fn + 10) // 40
除此之外,當我們重寫函式的valueOf方法也能夠改變函式的隱式轉換結果。
function fn() { return 20; }
fn.valueOf = function() { return 60 }
console.log(fn + 10) // 70
補充了這兩個知識點之後,我們可以來嘗試完成之前的題目了。add方法的實現仍然會是一個引數的收集過程。當add函式執行到最後時,仍然返回的是一個函式,但是我們可以通過定義toString/valueOf的方式,讓這個函式可以直接參與計算,並且轉換的結果是我們想要的。而且它本身也仍然可以繼續執行接收新的引數。實現方式如下
function add() {
// 第一次執行時,定義一個陣列專門用來儲存所有的引數
var _args = [].slice.call(arguments);
// 在內部宣告一個函式,利用閉包的特性儲存_args並收集所有的引數值
var adder = function () {
var _adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return _adder;
};
// 利用隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
// return adder.apply(null, _args);
return adder(..._args);
}
var a = add(1)(2)(3)(4);
var b = add(1, 2)(3, 4);
// 可以利用隱式轉換的特性參與計算
console.log(a + 10); // 20
// 也可以繼續傳入引數,得到的結果再次利用隱式轉換參與計算
console.log(a(10) + 100); // 120
參考資料:
好早之前做的筆記,找不到了
相關文章
- JS:函式柯里化JS函式
- JS高階函式-函式柯里化JS函式
- JS 分步實現柯里化函式JS函式
- JS專題之函式柯里化JS函式
- js柯里化函式的好處JS函式
- 函式柯里化函式
- JS中的 偏函式 和 柯里化JS函式
- JavaScript函式柯里化JavaScript函式
- 高階函式應用 —— 柯里化與反柯里化函式
- 函式的合成與柯里化函式
- JavaScript函式柯里化詳解JavaScript函式
- JavaScript函式柯里化的作用JavaScript函式
- 前端之函式柯里化Currying前端函式
- 函式柯里化和偏函式應用函式
- [譯] JavaScript中的函式柯里化JavaScript函式
- [譯] 柯里化與函式組合函式
- JavaScript進階之函式柯里化JavaScript函式
- 常用JS函式-陣列扁平化,快取函式,柯里化函式,防抖和節流函式JS函式陣列快取
- 用大白話介紹柯里化函式函式
- 柯里化與反柯里化
- 深入理解javascript系列(十七):函式柯里化JavaScript函式
- 前端戰五渣學JavaScript——函式柯里化前端JavaScript函式
- 「前端進階」徹底弄懂函式柯里化前端函式
- 手寫系列:call、apply、bind、函式柯里化APP函式
- js函數語言程式設計(二)-柯里化JS函數程式設計
- 一段柯里化函式程式碼閱讀函式
- 深入 call、apply、bind、箭頭函式以及柯里化APP函式
- 從 ES6 高階箭頭函式理解函式柯里化函式
- 柯里化
- 打造屬於自己的underscore系列(五)- 偏函式和函式柯里化函式
- 用場景去理解函式柯里化(入門篇)函式
- 「前端面試題系列6」理解函式的柯里化前端面試題函式
- JavaScript函數語言程式設計(純函式、柯里化以及組合函式)JavaScript函數程式設計函式
- 『手撕Vue-CLI』函式柯里化最佳化程式碼Vue函式
- 從一道面試題認識函式柯里化面試題函式
- javascript中bind繫結接收者與函式柯里化JavaScript函式
- JavaScript中的事件迴圈機制跟函式柯里化JavaScript事件函式
- 理解柯里化