最近面試的時候遇到了一個面試題:add(1)(2)(3)(4)輸出結果為10。
const addFn = (...args) => args.reduce((total, cur) => total + cur, 0)
const curry = (fn) => {
// Your Code Here
}
const add = curry(addFn)
const value = add(1,2)(3)(4)()
console.log(value) // 10
看到這道面試題的時候,有點迷茫,不知所措~~~
就像是寶強在《人在囧途》中的反應:啥!啥!啥!這寫的都是啥?
一開始,我發現1+2+3+4=10,寫了以下的代碼
function add (a, b, c, d) {
return a + b + c + d
}
add(1, 2, 3, 4) // 10
add(1,2)(3)(4)()//10
function add (a) {
return function (b) {
return function (c) {
return function (d) {
return a + b + c + d
}
}
}
}
又覺得不會這么簡單吧,要是累加到100、1000呢~
沒有思路~
面試結束后,在網上找相關的知識點學習,了解到一個概念叫:函數柯里化。
什么是函數柯里化(curry)
函數柯里化(curry)是函數式編程里面的概念。
curry的概念很簡單:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
簡單點來說就是:每次調用函數時,它只接受一部分參數,并返回一個函數,直到傳遞所有參數為止。
舉個?? 將下面接受兩個參數的函數改為接受一個參數的函數。
const add = (x, y) => x + y;
add(1, 2);
改成每次只接受一個參數的函數
const add = x => y => x + y;
add(1)(2);`
我們可以自己先嘗試寫一個add(1)(2)(3)
const add = x => y => z => x + y + z;
console.log(add(1)(2)(3));
看起來并不是那么難,但是如果面試官的要求是實現一個add 函數,同時支持下面這幾種的用法呢
add(1, 2, 3);
add(1, 2)(3);
add(1)(2, 3);
如果還是按照上面的這種思路,我們是不是要寫很多種呢...
我們當然可以自己實現一個工具函數專門來生成柯里化函數。
主要思路是什么呢,要判斷當前傳入函數的參數個數 (args.length) 是否大于等于原函數所需參數個數 (fn.length) ,如果是,則執行當前函數;如果是小于,則返回一個函數。
const curry = (fn, ...args) =>
// 函數的參數個數可以直接通過函數數的.length屬性來訪問
args.length >= fn.length // 這個判斷很關鍵!!!
// 傳入的參數大于等于原始函數fn的參數個數,則直接執行該函數
? fn(...args)
/**
* 傳入的參數小于原始函數fn的參數個數時
* 則繼續對當前函數進行柯里化,返回一個接受所有參數(當前參數和剩余參數) 的函數
*/
: (..._args) => curry(fn, ...args, ..._args);
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
柯里化有什么作用
主要有3個作用: 參數復用、提前返回和 延遲執行
我們來簡單的解釋一下: 參數復用:拿上面 f
這個函數舉例,只要傳入一個參數 z
,執行,計算結果就是 1 + 2 + z
的結果,1 和 2 這兩個參數就直接可以復用了。
提前返回 和 延遲執行 也很好理解,因為每次調用函數時,它只接受一部分參數,并返回一個函數(提前返回),直到(延遲執行)傳遞所有參數為止。
函數柯里化解決方案
函數柯里化有兩種不同的場景,一種為函數參數個數定長的函數,另外一種為函數參數個數不定長的函數。
函數參數個數定長的柯里化解決方案
// 定長參數
function add (a, b, c, d) {
return [
...arguments
].reduce((a, b) => a + b)
}
function currying (fn) {
let len = fn.length
let args = []
return function _c (...newArgs) {
// 合并參數
args = [
...args,
...newArgs
]
// 判斷當前參數集合args的長度是否 < 目標函數fn的需求參數長度
if (args.length < len) {
// 繼續返回函數
return _c
} else {
// 返回執行結果
return fn.apply(this, args.slice(0, len))
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同時支持addCurry(1)(2, 3)(4)該方式調用
console.log(total) // 10
函數參數個數不定長的柯里化解決方案
問題升級:那這個問題再升級一下,函數的參數個數不確定時,如何實現呢?
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
// 注意調用方式的變化
console.log(addCurry(1)(2)(3)(4, 5)())
函數柯里化是一種技術,一種將多入參函數變成單入參函數。
這樣做會讓函數變得更復雜,但同時也提升了函數的普適性。
舉個例子
//正常函數
function sum(a,b){
console.log(a+b);
}
sum(1,2); //輸出3
sum(1,3); //輸出4
//柯里化函數
function curry(a){
return (b) =>{
console.log(a+b)
}
}
const sum = curry(1);
sum(2); //輸出3
sum(3); //輸出4
例子里,為使用柯里化的函數在入參的時候即使在某一個入參是固定的情況下。也需要一樣的去輸入,那么這個輸入就變得冗余了。
柯里化之后的函數可以省略掉一個固定的入參。
但到這里,還有一個問題。現在只是一層封裝的柯里化。如果是四層,五層呢。
假設有這樣一個場景
//柯里化之前
function sum(a,b,c,d,e){
console.log(a+b+c+d+e)
}
sum(1,2,3,4,5);
//柯里化
function sum1(a){
return function sum2(b){
return function sum3(c){
return function sum4(d){
return function sum5(e){
console.log(a+b+c+d+e)
}
}
}
}
}
sum1(1)(2)(3)(4)(5);
多層柯里化的時候代碼會不美觀,可讀性非常差。
但需求總是在的。我們總會需要多層柯里化的時候。
所有,我們可以封裝一個函數來幫助我們完成函數向柯里化的轉換。
//函數柯里化封裝(這個封裝可以直接復制走使用)
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function () {
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
}
//需要被柯里化的函數
function multiFn(a, b, c) {
return a * b * c;
}
//multi是柯里化之后的函數
var multi = curry(multiFn);
console.log(multi(2)(3)(4));
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
柯里化的應用場景
其實柯里化大多是情況下是為了減少重復傳遞的不變參數。
舉個最簡單的例子吧。手機號正則校驗。
//校驗手機號
function validatePhone(regExp,warn,phone){
const reg = regExp;
if (phone && reg.test(phone) === false) {
return Promise.reject(warn);
}
return Promise.resolve();
}
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",187****3311)
這種寫法乍一看好像沒什么問題。但是,如果你需要多次調用呢?
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",137****1234)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",159****6204)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",137****2125)
//調用校驗
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符",191****5236)
會發現,正則和提示入參是固定的。很冗余。
我們可以使用我們上面封裝的柯里化工具(curry函數)進行如下修改。
//完成柯里化
const curryValid = curry(validatePhone);
const validatePhoneCurry =curryValid(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手機號格式不符");
//調用柯里化之后的函數
validatePhoneCurry(159****6204);
validatePhoneCurry(137****1234);
validatePhoneCurry(137****2125);
validatePhoneCurry(191****5236);
如上,我們可以省略很多不必要的參數。
當然,柯里化的應用場景還有延時執行(閉包也可以實現,而且更簡單),還有提前返回(主要針對IE,IE也馬上退休了,這里不認為有贅述的意義)