這是前端面試題系列的第 6 篇,你可能錯過了前面的篇章,能夠在這裏找到:前端
最近,朋友T 在準備面試,他爲一道編程題所困,向我求助。原題以下:面試
// 寫一個 sum 方法,當使用下面的語法調用時,能正常工做 console.log(sum(2, 3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5
這道題要考察的,就是對函數柯里化的理解。讓咱們先來解析一下題目的要求:編程
因此,sum 函數能夠這樣寫:segmentfault
function sum (x) { if (arguments.length == 2) { return arguments[0] + arguments[1]; } return function(y) { return x + y; } }
arguments 的用法挺靈活的,在這裏它則用於分割兩種不一樣的狀況。當參數只有一個的時候,進行柯里化的處理。數組
那麼,到底什麼是函數的柯里化呢?接下來,咱們將從概念出發,探究函數柯里化的實現與用途。緩存
柯里化,是函數式編程的一個重要概念。它既能減小代碼冗餘,也能增長可讀性。另外,附帶着還能用來裝逼。app
先給出柯里化的定義:在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。函數式編程
柯里化的定義,理解起來有點費勁。爲了更好地理解,先看下面這個例子:函數
function sum (a, b, c) { console.log(a + b + c); } sum(1, 2, 3); // 6
毫無疑問,sum 是個簡單的累加函數,接受3個參數,輸出累加的結果。佈局
假設有這樣的需求,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 指的就是本文一開始的源處理函數 sum
。currArgs 是調用 curry 時傳入的參數列表,好比 (1, 2)(3)
這樣的。
再看到 curry 函數內部,它會整個返回一個匿名函數。
再接下來的 let args = [].slice.call(arguments);
,意思是將 arguments 數組化。arguments 是一個類數組的結構,它並非一個真的數組,因此無法使用數組的方法。咱們用了 call 的方法,就能愉快地對 args 使用數組的原生方法了。在這篇 「乾貨」細說 call、apply 以及 bind 的區別和用法 中,有關於 call 更詳細的用法介紹。
currArgs !== undefined
的判斷,是爲了解決遞歸調用時的參數拼接。
最後,判斷 args 的個數,是否與 fn (也就是 sum )的參數個數相等,相等了就能夠把參數都傳給 fn,進行輸出;不然,繼續遞歸調用,直到二者相等。
測試一下:
function sum(a, b, c) { console.log(a + b + c); } const fn = curry(sum); fn(1, 2, 3); // 6 fn(1, 2)(3); // 6 fn(1)(2, 3); // 6 fn(1)(2)(3); // 6
都能輸出 6 了,搞定!
理解了柯里化的實現以後,讓咱們來看一下它的實際應用。柯里化的目的是,減小代碼冗餘,以及增長代碼的可讀性。來看下面這個例子:
const persons = [ { name: 'kevin', age: 4 }, { name: 'bob', age: 5 } ]; // 這裏的 curry 函數,以前已實現 const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; }); const ages = persons.map(getProp('age')); // [4, 5] const names = persons.map(getProp('name')); // ['kevin', 'bob']
在實際的業務中,咱們常會遇到相似的列表數據。用 getProp 就能夠很方便地,取出列表中某個 key 對應的值。
須要注意的是,const names = persons.map(getProp('name'));
執行這條語句時 getProp 的參數只有一個 name
,而定義 getProp 方法時,傳入 curry 的參數有2個,obj
和 index
(這裏必須寫 2 個及以上的參數)。
爲何要這麼寫?關鍵就在於 arguments
的隱式傳參。
const getProp = curry(function (obj, index) { console.log(arguments); // 會輸出4個類數組,取其中一個來看 // { // 0: {name: "kevin", age: 4}, // 1: 0, // 2: [ // {name: "kevin", age: 4}, // {name: "bob", age: 5} // ], // 3: "age" // } });
map 是 Array 的原生方法,它的用法以下:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]);
因此,咱們傳入的 name
,就排在了 arguments 的最後。爲了拿到 name
對應的值,須要對類數組 arguments 作點轉換,讓它可使用 Array 的原生方法。因此,最終 getProp 方法定義成了這樣:
const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; });
固然,還有另一種寫法,curry 的實現更好理解,可是調用的代碼卻變多了,你們能夠根據實際狀況進行取捨。
const getProp = curry(function (key, obj) { return obj[key]; }); const ages = persons.map(item => { return getProp(item)('age'); }); const names = persons.map(item => { return getProp(item)('name'); });
最後,來看一個 Memoization 的例子。它用於優化比較耗時的計算,經過將計算結果緩存到內存中,這樣對於一樣的輸入值,下次只須要中內存中讀取結果。
function memoizeFunction(func) { const cache = {}; return function() { let key = arguments[0]; if (cache[key]) { return cache[key]; } else { const val = func.apply(null, arguments); cache[key] = val; return val; } }; } const fibonacci = memoizeFunction(function(n) { return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2); }); console.log(fibonacci(100)); // 輸出354224848179262000000 console.log(fibonacci(100)); // 輸出354224848179262000000
代碼中,第2次計算 fibonacci(100) 則只須要在內存中直接讀取結果。
函數的柯里化,是 Javascript 中函數式編程的一個重要概念。它返回的,是一個函數的函數。其實現方式,須要依賴參數以及遞歸,經過拆分參數的方式,來調用一個多參數的函數方法,以達到減小代碼冗餘,增長可讀性的目的。
雖然一開始理解起來有點雲裏霧裏的,但一旦理解了其中的含義和具體的使用場景,用起來就會駕輕就熟了。
PS:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。