關於函數柯里化的定義,我摘抄一段來自百度百科的原話:在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受余下的參數且返回結果的新函數的技術。
這段話聽起來可能有一些抽象,但是如果用實際例子來解釋可能會幫助我們更好地理解何為函數柯里化。看看下面這個問題,是一道前端面試中常考的題:
如何實現add(2)(3)(4) = 9
當我第一次看到這個題目的時候我就在思考,add(2)后面為什么還能帶(3)(4)呢?是不是因為add(2)返回的是一個函數,所以它后面可以繼續接收參數,可是如果這樣的話它要怎么返回具體數值呢?於是我想到了一個可以實現儲存參數的關鍵 - 閉包!是經過自己無數次嘗試以及嘗試失敗之后,最終向命運妥協,開始到網上查閱資料,於是發現了以下這段代碼,讀者可以先仔細閱讀一下下面這段代碼,后面我們再來慢慢解析究竟何為函數柯里化,為什么函數柯里化就能解決這個問題?
1 let myAdd = (a, b, c) => a+b+c; 2 function curry(fn, args){ 3 let len = fn.length; 4 let _this = this; 5 let _args = args || []; 6 return function(){ 7 let args = Array.prototype.slice.apply(arguments); 8 args = Array.prototype.concat.call(_args, args); 9 // 當接收到的參數小於fn所需參數個數時,繼續接收參數 10 if(args.length < len){ 11 return curry.call(_this, fn, args); 12 } 13 return fn.apply(this, args); 14 } 15 } 16 let add = curry(myAdd); 17 console.log(add(2)(3)(4)); // 9 18 console.loh(add(2,3)(4)); // 9 19 console.log(add(2,3,4)); // 9
在上面的代碼中,myAdd是一個可以接收三個參數並且返回三數之和的函數。通過一頓操作之后,它可以不用一次性接收三個參數,而是慢慢接收,當發現接收到的參數達到3個之后再返回結果。這就涉及到函數柯里化的第一個特點 --- 參數復用(后面再進行詳解)。通過例子我們可以看到,實現這個特點主要原因是利用了閉包,將接收到的參數存於_args中,由於閉包的原因這些參數在函數執行完之后並不會被釋放掉。
上面的curry方法,將每次調用fn時讀入的參數用args來保存,並將本次讀入的參數args與當前一共擁有的所有參數_args用concat方法連接起來,當參數個數符合fn的參數個數要求時,則調用fn。(筆者不才,沒辦法想到用什么通俗易懂的語言來描述這個過程,只能將自己的理解表述出來)
但是這個例子並不具有普遍性,如果我需要傳入的參數不為3個那怎么辦,所以對上面的例子進行修改之后可以得到下面這段代碼
function add() { // 第一次執行時,定義一個數組專門用來存儲所有的參數 var _args = Array.prototype.slice.call(arguments); // 在內部聲明一個函數,利用閉包的特性保存_args並收集所有的參數值 var _adder = function() { _args.push(...arguments); return _adder; }; // 利用toString隱式轉換的特性,當最后執行時隱式轉換,並計算最終的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } add(1)(2)(3) // 6 add(1, 2, 3)(4) // 10 add(1)(2)(3)(4)(5) // 15 add(2, 6)(1) // 9
上面這種寫法存在一個弊端,就是返回值的類型是函數而不是數值,但是筆者目前也沒找到什么更合適的方法了。
解析完例子,我們來看看究竟什么是函數柯里化。柯里化一共有三大作用,分別是:
- 參數復用
- 提前確認
- 延遲運行
一、參數復用
所謂參數復用,就是利用閉包的原理,讓我們前面傳輸過來的參數不要被釋放掉。看一下下面這段代碼就顯而易見了:
1 // 正常封裝check函數進行字符串正則匹配 2 function check(reg, txt) { 3 return reg.test(txt) 4 } 5 6 check(/\d+/g, 'test') //false 7 check(/[a-z]+/g, 'test') //true 8 9 // 使用柯里化函數進行字符串正則匹配 10 function curryingCheck(reg) { 11 return function(txt) { 12 return reg.test(txt) 13 } 14 } 15 16 var hasNumber = curryingCheck(/\d+/g) 17 var hasLetter = curryingCheck(/[a-z]+/g) 18 19 hasNumber('test1') // true 20 hasNumber('testtest') // false 21 hasLetter('21212') // false
二、 提前確認
這一特性經常是用來對瀏覽器的兼容性做出一些判斷並初始化api,比如說我們目前用來監聽事件大部分情況是使用addEventListener來實現的,但是一些較久的瀏覽器並不支持該方法,所以在使用之前,我們可以先做一次判斷,之后便可以省略這個步驟了。
1 var on = (function() { 2 if (document.addEventListener) { 3 return function(element, event, handler) { 4 if (element && event && handler) { 5 element.addEventListener(event, handler, false); 6 } 7 }; 8 } else { 9 return function(element, event, handler) { 10 if (element && event && handler) { 11 element.attachEvent('on' + event, handler); 12 } 13 }; 14 } 15 })();
三、 延遲運行
js中的bind這個方法,用到的就是柯里化的這個特征。
1 Function.prototype.bind = function (context) { 2 var _this = this 3 var args = Array.prototype.slice.call(arguments, 1) 4 5 return function() { 6 return _this.apply(context, args) 7 } 8 }