基礎概念
當一個函數有多個參數的時候,先傳遞一部分參數調用他(這部分參數以后永遠不變),然后返回一個新的函數接受剩余的參數,返回結果;
簡言之就是:多變量函數拆解為單變量的多個函數的依次調用;
可以干嘛呢?
可以利用它來實現對函數參數的緩存,降低函數粒度,把多元函數轉換成一元函數,實現函數的組合,產生更強大的功能
核心流程分析
就是利用閉包和遞歸調用,可以形成一個不銷毀的私有作用域,把預先處理的內容放到不銷毀的作用域里面,返回一個函數供以后調用;
舉個例子:
比如我們有一個判斷用戶年齡是否大于某個值的函數
// 普通的純函數
function checkAge (min, age) {
return age >= min
}
// 普通調用
console.log(checkAge(18, 20)) //true
console.log(checkAge(18, 24)) //true
console.log(checkAge(60, 30)) //false
可能需要經常判斷用戶是否成年(大于18歲),為了減少代碼重復,所以改造如下
// 柯里化后的函數
function checkAge (min) {
return function (age) {
return age >= min
}
}
const checkAge18 = checkAge(18)
const checkAge60 = checkAge(60)
console.log(checkAge18(20)) //true
console.log(checkAge18(24)) //true
console.log(checkAge60(30)) //false
以上就是一個針對checkAge函數的柯里化改造,他的自由度很低,因此需要封裝一個通用的柯里化函數;
實現思路
首先,我們通過調用lodash提供的柯里化函數(curry)來了解一下如何使用,并且分析一下實現思路
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
// 定義一個柯里化函數
const curried = _.curry(getSum)
// 如果輸入了全部的參數,則立即返回結果
console.log(curried(1, 2, 3)) // 6
//如果傳入了部分的參數,此時它會返回當前函數,并且等待接收getSum中的剩余參數
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6</pre>
通過以上可以看出,柯里化函數的運行過程其實是一個參數的收集過程,將每一次傳入的參數收集起來,在最后統一處理
所以,實現思路:
- 調用curry,傳遞一個函數,然后需要返回一個柯里化函數(curried)
- 如果調用curried傳遞的參數和getSum參數個數相同,就立即執行并返回結果;如果調用curried傳遞的是部分參數,那么需要返回一個新函數,等待接受getSum其他參數
具體實現如下:
function curry(func) {
return function curriedFn(...args) {
// 若實參的個數小于形參的個數
if (args.length < func.length) {
return function () {
// 等待傳遞的剩余參數
// 第一部分參數在args里面,第二部分參數在arguments里面
return curriedFn(...args.concat(...arguments));
};
}
// 如果實參大于等于形參的個數,立即執行并返回結果
// args是剩余參數
return func(...args);
};
}
注意:這里有個細節,就是要柯理化的函數不能有默認值,否則該函數的length屬性將失真;
將造成結果提前返回或者報錯
如下:
- image
該技術的優缺點
上面費那么大勁封裝,到底有什么好處呢?
優點:
參數復用;參考上面的checkAge函數,把18這個參數緩存起來,多個地方用到18的就可以直接調用
將多元函數比變成一元函數,然后組合函數產生更強大功能
延遲運行;像經常使用的bind,就是基于柯里化實現的;
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
那缺點也顯然易見:
- 使用了大量的閉包,內存得不到釋放,容易造成內存泄漏
對比傳統的函數調用,則不會產生閉包,使用完即可釋放
其實在大部分應用中,主要的性能瓶頸是在操作DOM節點上,這js的性能損耗基本是可以忽略不計的,只要注意閉包的內存釋放即可放心使用。
面試題
一)
// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1,2,3) = 6;
add(1,2)(3) = 6;
add(1)(2)(3) = 6;
這個題目是想讓add函數執行后,返回一個能夠繼續執行的函數,最終計算出所有參數的和,重點在于每次接受的參數可以有一個,也可以有多個(add接受的參數個數固定);
答案如下:
function curry(func) {
return function curriedFn(...args) {
// 若實參的個數小于形參的個數
if (args.length < func.length) {
return function () {
// 等待傳遞的剩余參數
// 第一部分參數在args里面,第二部分參數在arguments里面
return curriedFn(...args.concat(...arguments));
};
}
// 如果實參大于等于形參的個數,立即執行并返回結果
// args是剩余參數
return func(...args);
};
}
function add(a,b,c){
return a+b+c;
}
const newFn = curry(add)
newFn(1)(2)(3) //6
newFn(1,2)(3) //6
newFn(1,2,3) //6
上述考題是參數固定:也就是add已知參數就是3個;那參數不固定的,如何解決呢?請看第2題
二)
// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
這個題目相較于第1題,它的難點在于add的參數不固定;所以要繼續優化;
先來看下面兩種解法
解法1.
// 柯里化寫法
function sum(...arr) {
return arr.reduce((per, next) => {
return per + next;
}, 0);
}
function curry(fn) {
let args = [];
return function curried(...res) {
if (res.length) {
args = [...args, ...res];
return curried;
} else {
return fn.apply(this, args);
}
};
}
let add = curry(sum);
console.log(add(1)(2)(3)()); //6
解法2.
//toString 寫法
function curry(a) {
function curried(item) {
a += item;
return curried;
}
curried.toString = function () {
return a;
};
return curried;
}
console.log(curry(1)(2)(3).toString()); //6
以上兩種方式雖然都能實現,但是解法1需要最后再調用一次,而解法2需要多調用一個轉換函數;
都有點勉強,不太符合考題調用方式;
那來看最后一種實現方式:
解法3.
function add(...args) {
let final = [...args];
setTimeout(() => {
console.log(final.reduce((sum, cur) => sum + cur));
}, 0);
const inner = function (...args) {
final = [...final, ...args];
return inner;
};
return inner;
}
console.log(add(1)(2)(3)); //6
這個方法利用了異步編程,setTimeout中的內容延遲執行,算是個奇淫技巧,但終歸是符合了考題的調用方法;
具體使用哪種,還要看面試官想考什么?
如果是考柯里化知識點,那就選解法1;
如果必須按照題目方式調用,那只能選擇解法3