JavaScript函數柯裡化詳解

一、簡單瞭解apply和call

  • call 和 apply 都是為瞭改變某個函數運行時的 context 即上下文而存在的,換句話說,就是為瞭改變函數體內部 this 的指向。
  • call 和 apply二者的作用完全一樣,隻是接受參數的方式不太一樣。call其實是apply的一種語法糖。
  • 格式:apply(context,[arguments]),call(context,param1,param2,...)

二、什麼是函數柯裡化?

柯裡化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數且返回結果的新函數的技術。

在這裡舉個例子,有一個add()函數,它是用來處理我們傳給它的參數(param1,params2,…)相加求和的一個函數。

// 在這裡第一個具有兩個參數`x`、`y`的`add(x , y)`函數
function add(x , y){
	return x + y;
}
// 調用`add()`函數,並給定兩個參數`4`和`6`
add(4,6);
// 模擬計算機操作,第一步 傳入第一個參數 4
function add(4 , y){
	return 4 + y;
}
// 模擬計算機操作,第二步 傳入第一個參數 6
function add(4 , 6){
	return 4 + 6;
}

如果我們將add()函數柯裡化,是什麼樣子呢?在這裡簡單的實現一下:

// 柯裡化過的add()函數,可以接受部分參數
function add(x ,y){
	if (typeof y === 'undefined') {
		return function (newy){
			return x + newy;
		}
	}
	// 完整應用
	return x + y;
}
// 測試調用
console.log(typeof add(4)); // [Function]
console.log(add(4)(6)); // 10
// 可以創建保存函數
let saveAdd = add(4);
console.log(saveAdd(6)); // 10

從以上簡單柯裡化的add()函數可以看出,函數可以接受部分函數,然後返回一個新的函數,使其繼續處理剩下的函數。

三、寫一個公共的柯裡化函數

在這裡我們創建一個公共的柯裡化函數,那樣我們就不必每次寫一個函數都要在其內部實現復雜的柯裡化過程。

// 定義一個createCurry的函數
function createCurry(fn){
	var slice = Array.prototype.slice,
	stored_args = slice.call(arguments,1);
	return function () {
		let new_args = slice.call(arguments),
		args = stored_args.concat(new_args);
		return fn.apply(null,args);
	}
}

在以上公共的柯裡化函數中:

  • arguments,並不是一個真的數組,隻是一個具有length屬性的對象,所以我們從Array.prototype中借用slice方法幫我們把arguments轉為一個真正的數組,方便我們更好的操作。
  • 當我們第一次調用函數createCurry的時候,其中變量stored_args 是保持瞭除去第一個參數以外的參數,因為第一個參數是我們需要柯裡化的函數。
  • 當我們執行createCurry函數中返回的函數時,變量new_args獲取參數並轉為數組。
  • 內部返回的函數通過閉包訪問變量stored_args中存儲的值和變量new_args的值合並為一個新的數組,並賦值給變量args
  • 最後調用fn.apply(null,args)方法,執行被柯裡化的函數。

現在我們來測試公共的柯裡化函數

// 普通函數add()
function add(x , y){
	return x + y;
}
// 柯裡化得到一個新的函數
var newAdd = createCurry(add,4);
console.log(newAdd(6)); // 10

//另一種簡便方式
console.log(createCurry(add,4)(6));// 10

當然這裡並不局限於兩個參數的柯裡化,也可以多個參數:

// 多個參數的普通函數
function add(a,b,c,d){
	return a + b + c + d;
}
// 柯裡化函數得到新函數,多個參數可以隨意分割
console.log(createCurry(add,4,5)(5,6)); // 20
// 兩步柯裡化
let add_one = createCurry(add,5);
console.log(add_one(5,5,5));// 20
let add_two = createCurry(add_one,4,6);
console.log(add_two(6)); // 21

通過以上的例子,我們可以發現一個局限,那就是不管是兩個參數還是多個參數,它隻能分兩步執行,如以下公式:

  • fn(x,y) ==> fn(x)(y);
  • fn(x,y,z,w) ==> fn(x)(y,z,w) || fn(x,y)(z,w)||…

如果我們想更靈活一點:

  • fn(x,y) ==> fn(x)(y);
  • fn(x,y,z) ==> fn(x,y)(z) || fn(x)(y)(z);
  • fn(x,y,z,w) ==> fn(x,y)(z)(w) || fn(x)(y)(z)(w) || …;

我們該怎麼實現呢?

四、創建一個靈活的柯裡化函數

經過以上練習,我們發現我們創建的柯裡化函數存在一定局限性,我們希望函數可以分為多步執行:

// 創建一個可以多步執行的柯裡化函數,當參數滿足數量時就去執行它:
// 函數公式:fn(x,y,z,w) ==> fn(x)(y)(z)(w);
let createCurry = (fn,...params)=> {
	let args = parsms || [];
	let fnLen = fn.length; // 指定柯裡化函數的參數長度
	return (...res)=> {
		// 通過作用域鏈獲取上一次的所有參數
		let allArgs = args.slice(0);
		// 深度拷貝閉包共用的args參數,避免後續操作影響(引用類型)
		allArgs.push(...res);
		if(allArgs.length < fnLen){
		   // 當參數數量小於原函數的參數長度時,遞歸調用createCurry函數
		   return createCurry.call(this,fn,...allArgs);
		}else{
		  // 當參數數量滿足時,觸發函數執行
		  return fn.apply(this,allArgs);
		}
	}
}

// 多個參數的普通函數
function add(a,b,c,d){
	return a + b + c + d;
}
// 測試柯裡化函數
let curryAdd = createCurry(add,1);
console.log(curryAdd(2)(3)(4)); // 10

以上我們已經實現瞭靈活的柯裡化函數,但是這裡我們又發現瞭一個問題:

  • 如果我第一次就把參數全部傳入,但是它並沒有返回結果,而是一個函數(function)。
  • 隻有我們再次將返回的函數調用一次才能返回結果:curryAdd(add,1,2,3,4)();
  • 可能有人說如果是全部傳參,就調用原來的add()函數就行瞭,這也是一種辦法;但是我們在這裡既然是滿足參數數量,對於這種情況我們還是處理一下。

在這裡我們隻需要在返回函數前做一下判斷就行瞭:

let createCurry = (fn,...params)=> {
	let args = parsms || [];
	let fnLen = fn.length; // 指定柯裡化函數的參數長度
	if(length === _args.length){
	   // 加入判斷,如果第一次參數數量以經足夠時就直接調用函數獲取結果
           return fn.apply(this,args);
        }
	return (...res)=> {
		let allArgs = args.slice(0);
		allArgs.push(...res);
		if(allArgs.length < fnLen){
		   return createCurry.call(this,fn,...allArgs);
		}else{
		  return fn.apply(this,allArgs);
		}
	}
}

以上可以算是完成瞭一個靈活的柯裡化的函數瞭,但是這裡還不算很靈活,因為我們不能控制它什麼時候執行,隻要參數數量足夠它就自動執行。我們希望實現一個可以控制它執行的時機該怎麼辦呢?

五、寫一個可控制的執行時間的柯裡化函數

我們這裡直接說明一下函數公式:

  • fn(a,b,c) ==> fn(a)(b)(c )();
  • fn(a,b,c) ==> fn(a);fn(b);fn(c );fn();
  • 當我們參數足夠時它並不會執行,隻有我們再次調用一次函數它才會執行並返回結果。在這裡我們在以上例子中加一個小小的條件就可以實現。
// 當參數滿足,再次執行時調用函數
let createCurry = (fn,...params)=> {
	let args = parsms || [];
	let fnLen = fn.length; // 指定柯裡化函數的參數長度
	//當然這裡的判斷需要註釋掉,不然當它第一次參數數量足夠時就直接執行結果瞭
	//if(length === _args.length){
	   // 加入判斷,如果第一次參數數量以經足夠時就直接調用函數獲取結果
           //return fn.apply(this,args);
        //}
	return (...res)=> {
		let allArgs = args.slice(0);
		allArgs.push(...res);
		// 在這裡判斷輸入的參數是否大於0,如果大於0在判斷參數數量是否足夠,
		// 這裡不能用 && ,如果用&& 也是參數數量足夠時就執行結果瞭。
		if(res.length > 0 || allArgs.length < fnLen){
		   return createCurry.call(this,fn,...allArgs);
		}else{
		  return fn.apply(this,allArgs);
		}
	}
}

// 多個參數的普通函數
function add(a,b,c,d){
	return a + b + c + d;
}
// 測試可控制的柯裡化函數
let curryAdd = createCurry(add,1);
console.log(curryAdd(2)(3)(4)); // function
console.log(curryAdd(2)(3)(4)()); // 10
console.log(curryAdd(2)(3)()); // 當參數不足夠時返回 NaN

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: