【 js 基礎 】【 源碼學習 】柯裏化和箭頭函數
最近在看 redux 的源碼,代碼結構很簡單,主要就是6個文件,其中 index.js 負責將剩余5個文件中定義的方法 export 出來,其他5個文件各自負責一個方法的實現。
大部分代碼比較簡單,很容易看懂,但是在 applyMiddleware.js 中 有一個地方還是很有意思,用到了柯裏化和箭頭函數的組合。由於增強 store,豐富 dispath 方法的時候,可能會用到多個 中間件,所以這個的嵌套有可能會很深,導致對 箭頭函數和柯裏化 不是很熟悉的童鞋,一看源碼就會有些理不清思路。
一、柯裏化
是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受余下的參數而且返回結果的新函數的技術。
--來自 wiki
舉個例子:
柯裏化前:
1 function add(a, b) { 2 return a + b; 3 } // 執行 add 函數,一次傳入兩個參數即可 4 add(10, 2) // 12
柯裏化後:
1 var add = function(a) { 2 return function(b) { 3 return a + b; 4 }; 5 }; 6 var addTen = add(10); 7 addTen(2); // 12
那麽我們為什麽要使用柯裏化呢?
首先柯裏化可以使得參數復用並且延遲計算,也就是說像上面的例子,比如我們的 a 值一直都是 10,只是 b 值在變化,那麽這個時候用到柯裏化,就可以減少一些重復性的傳參。
比如 b 是 2,3,4
那麽在柯裏化前的調用就是:
add(10,2) add(10,3) add(10,4)
而在柯裏化之後,就像上面的例子中寫的,我們通過定義
var addTen = add(10);
將第一個參數 a 預置了 10,
之後我們只需要調用
1 addTen(2) 2 addTen(3) 3 addTen(4)
就可以了。
而在這個過程中,如果使用柯裏化前的代碼,或當即就把結果計算出來,而在柯裏化之後,我們可以現傳入一個10,然後在想得到真正結果的時候再傳入另一個參數。
同時柯裏化函數還可以 提前返回,很常見的一個例子,兼容現代瀏覽器以及IE瀏覽器的事件添加方法。我們正常情況不使用柯裏化可能會這樣寫
1 varaddEvent = function(el, type, fn, capture) { 2 if (window.addEventListener) { 3 el.addEventListener(type, function(e) { 4 fn.call(el, e); 5 }, capture); 6 } else if (window.attachEvent) { 7 el.attachEvent("on" + type, function(e) { 8 fn.call(el, e); 9 }); 10 } 11 };
這個時候我們沒調用一次 addEvent,就會進行一次 if else 的判斷,而其實具體用哪個方法進行方法的綁定的判斷執行一次就已經知道了,所以我們可以使用柯裏化來解決這個問題:
1 var addEvent = (function(){ 2 if (window.addEventListener) { 3 return function(el, sType, fn, capture) { 4 el.addEventListener(sType, function(e) { 5 fn.call(el, e); 6 }, (capture)); 7 }; 8 } else if (window.attachEvent) { 9 return function(el, sType, fn, capture) { 10 el.attachEvent("on" + sType, function(e) { 11 fn.call(el, e); 12 }); 13 }; 14 } 15 })();
一開始的自執行函數,完成了對 addEvent 具體使用 哪個方法的判斷,之後在調用傳參的時候都是直接給了已經判斷好的返回方法,所以使用了柯裏化 減少了我們每次的判斷,提前返回了我們需要的具體方法。
這裏還要多提一點,我們也經常會聽到高階函數,那麽高階函數和柯裏化是什麽關系?
所謂高階函數,即至少滿足下列的一個條件的函數:
a、接受一個或多個函數作為輸入
b、輸出一個函數
這樣你就明白了,柯裏化其實是高階函數的一種實現。
二、箭頭函數
箭頭函數是ES6中新增的函數形式。 他的語法是:1 (參數1, 參數2, …, 參數N) => {函數聲明} 2 (參數1, 參數2, …, 參數N) => 表達式(單一) 3 //相當於:(參數1, 參數2, …, 參數N) =>{ return 表達式 } 4 5 // 當只有一個參數時,圓括號是可選的: 6 (單一參數) => {函數聲明} 7 單一參數 => {函數聲明} 8 9 // 沒有參數的函數應該寫成一對圓括號。 10 () => {函數聲明}
舉個例子:
1 var func = x => x * x; 2 // 簡寫函數 省略return 3 4 var func = (x, y) => { return x + y; }; 5 //常規編寫 明確的返回值
這個轉換成 es5 的格式就是
1 function (x) { 2 return x * x; 3 }
1 function (x, y) { 2 return x + y; 3 }
那麽我們為什麽要使用尖頭函數?
主要的好處有兩點:
1、更簡短的函數
2、不綁定this
第一個優點是很容易理解的,你觀察上面的例子,雖然很簡單,但明顯箭頭函數的寫法剩下了不少的代碼,對照 es5 來看寫的更加簡潔了。
而第二個優點:
在箭頭函數之前,每個新定義的函數都有它自己的 this值(在構造函數的情況下是一個新對象,在嚴格模式的函數調用中為 undefined),這個知識點,大家可以我的另一篇文章(http://www.cnblogs.com/lijiayi/p/this.html)
舉個例子:
1 function Person() { 2 // Person() 構造函數定義 `this`作為它自己的實例. 3 this.age = 0; 4 5 setInterval(function growUp() { 6 // 在非嚴格模式, growUp()函數定義 `this`作為全局對象, 7 // 與在 Person()構造函數中定義的 `this`並不相同. 8 this.age++; 9 }, 1000); 10 } 11 12 var p = new Person();
而我們通常會這樣解決 setInterval 中 this 的指向問題,將 this 賦值給變量 self
1 function Person() { 2 var self = this; 3 that.age = 0; 4 5 setInterval(function growUp() { 6 // 回調引用的是`that`變量, 其值是預期的對象. 7 self.age++; 8 }, 1000); 9 }
而在箭頭函數中,這個問題是不會出現的。因為箭頭函數不會創建自己的this,它使用封閉執行上下文的this值。因此,在下面的寫法中,傳遞給 setInterval 的函數內的 this 與封閉函數中的 this 值相同:
1 function Person(){ 2 this.age = 0; 3 4 setInterval(() => { 5 this.age++; // |this| 正確地指向person 對象 6 }, 1000); 7 } 8 9 var p = new Person();
那麽如果使用 call 和 apply 調用,會怎麽樣呢
1 var adder = { 2 base : 1, 3 4 add : function(a) { 5 var f = v => v + this.base; 6 return f(a); 7 }, 8 9 addThruCall: function(a) { 10 var f = v => v + this.base; 11 var b = { 12 base : 2 13 }; 14 15 return f.call(b, a); 16 } 17 }; 18 19 console.log(adder.add(1)); // 輸出 2 20 console.log(adder.addThruCall(1)); // 仍然輸出 2
也就是說通過 call() 或 apply() 方法調用一個函數時,只是傳入了參數而已,對 this 並沒有什麽影響。
關於 箭頭函數和柯裏化,就說這麽多,具體深入的學習大家可以去搜資料,因為有太多太多,寫的很好的資料。
三、柯裏化和箭頭函數的結合
先來一個簡單的例子,咱們就利用上面的 add 方法:
es5寫法:
1 var add = function(a) {
2 return function(b) {
3 return a + b;
4 };
5 };
而當你加上了箭頭
1 let add = a => b => a + b
就只有這樣簡單的一句。
目前比較起來,還是比較容易理解的,但是當沒有給你 es5 的寫法並且箭頭也更加多,柯裏化函數每層的調用、參數預置分散開來的時候,理解起來還是會有一些蒙的。
咱們來舉個例子,理清一下如何去理解這樣組合的思路。
1 //[p]roperty, [v]alue, [o]bject: 2 const is = p => v => o => o.hasOwnProperty(p) && o[p] == v; 3 4 // outer: p => [inner1 function, uses p] 5 // inner1: v => [inner2 function, uses p and v] 6 // inner2: o => o.hasOwnProperty(p) && o[p] = v;
這個函數看最後一個箭頭後面的表達式很容易理解,就是對傳進來的屬性和值,在傳進來的對象上的匹配情況,如果對象上有這個屬性,並且對應的值也和傳進來的值相等,就返回 true。
而在我們對這個函數的調用過程,可以靈活的分為三步。
第一步 傳入 property 即 p 參數 ,這會返回一個需要 v 和 o 作為參數的函數:
v => o => o.hasOwnProperty(p) && o[p] == v;
第二步 傳入 value 即 v 參數,這同樣會返回一個需要 o 作為參數的函數:
o => o.hasOwnProperty(p) && o[p] == v;
在這個時候,咱們的表達式裏只有 o 是未知的,所以當我們再調用一次,就會得到一個 boolean 結果。
也就是說如果有 n 個箭頭,那麽我們在 n-1 次調用之前,都只是在像 表達式裏傳參,而伴隨這第 n 次的調用 的傳參,就會直接返回結果。
這個的調用過程就像是撥洋蔥,一層層的調用函數,到最後一層返回結果。
那麽這樣寫有什麽好處呢?
1、減少代碼重復
2、提高代碼重用性
舉個例子來比較一下:
result = users .filter(x => x.hasOwnProperty(‘pets‘)) .filter(x => x.hasOwnProperty(‘title‘))
和
1 const has = p => o => o.hasOwnProperty(p); 2 result = users 3 .filter(has(‘pets‘)) 4 .filter(has(‘title‘)) 5 ...
優點還是很明顯的。
最後咱們再來說一下 redux 源碼中對以上知識的應用:
1 import compose from ‘./compose‘ 2 export default function applyMiddleware(...middlewares) { 3 return createStore => (...args) => { 4 const store = createStore(...args) 5 let dispatch = () => { 6 throw new Error( 7 `Dispatching while constructing your middleware is not allowed. ` + 8 `Other middleware would not be applied to this dispatch.` 9 ) 10 } 11 let chain = [] 12 13 const middlewareAPI = { 14 getState: store.getState, 15 dispatch: (...args) => dispatch(...args) 16 } 17 chain = middlewares.map(middleware => middleware(middlewareAPI)) 18 dispatch = compose(...chain)(store.dispatch) 19 20 return { 21 ...store, 22 dispatch 23 } 24 }
這個是 redux 的 源碼,這裏重點要講的是 第 17、18 行,就兩句話,實現了將傳入的多個中間件套在一起,層層返回結果,最終豐富了 dispatch 。
首先背景知識:
我問我們用來豐富 dispatch 的中間件都是按照一定規律編寫的,有固定傳參順序,
格式如下 const reduxMiddleware = ({dispatch, getState}[簡化的store]) => (next[上一個中間件的dispatch方法]) => (action[實際派發的action對象]) => {} 每個中間件接收 getState 和 dispatch 作為參數,並返回一個函數,該函數會被傳入下一個中間件的 dispatch 方法,並返回一個接收 action 的新函數,最後在調用一次,傳入 action 得到結果。 知道中間件編寫的固定格式之後,首先 17 行,使用 map 遍歷 傳進來的 多個 middlewares ,給每個中間件都傳入參數 middlewareAPI,也就是最後的 chain 是這樣的:
1 chain = [ 2 function middlewareCreator1(next) { 3 // with getState, dispath 4 5 }, 6 function middlewareCreator2(next) { 7 // with getState, dispath 8 9 }, 10 ... 11 ]
middlewareAPI 是一個對象,這個對象是中間件所需要的第一個參數 ({dispatch, getState}[簡化的store]) 。第17行的目的就是將每個中間件所需要的第一個參數預置了進去,這個時候每個中間件就會返回一個 需要 next 作為參數的函數,chain 就是由這些返回的函數而組成的一個數組。
之後執行了
dispatch = compose(...chain)(store.dispatch)
compose 是在第一行 引入的,它是用來組合函數,也就是將傳入的多個中間件套在一起。
咱們來看一下 compose 的源碼,看看它是如何組合的:
1 export default function compose(...funcs) { 2 // 如果什麽都沒有傳,則直接返回 參數 3 // return arg => arg 即 4 // return function (arg) { 5 // return arg; 6 // }; 7 if (funcs.length === 0) { 8 return arg => arg 9 } 10 // 如果funcs中只有一個中間件,那麽就直接返回這個 中間件 11 if (funcs.length === 1) { 12 return funcs[0] 13 } 14 15 // reduce() 方法對累加器和數組中的每個元素(從左到右)應用一個函數,將其減少為單個值。 16 return funcs.reduce((a, b) => (...args) => a(b(...args))) 17 }
重點在第16行。
可以看到 redux 源碼中在調用 compose 方法的時候 後面跟了兩個括號,也就是調用了兩次
第一次調用:
compose([a,b])
這個會返回
(...args) => a(b(...args))
把 a、b 換成剛才得到的 chain,即返回了一個需要 ...args 作為參數的函數:
(...args) => middlewareCreator1(middlewareCreator2(middlewareCreator3(...args)))
然後 第二次調用,
dispatch = compose(...chain)(store.dispatch)
將所需要的 ...args 傳入了進去,即未豐富之前的 dispatch,最終得到:
middlewareCreator1(middlewareCreator2(middlewareCreator3(store.dispatch)))
也就是在 17 、18 行之後,咱們的 dispatch
dispatch = middlewareCreator1(middlewareCreator2(middlewareCreator3(store.dispatch)))
你還記得咱們剛才講的中間件的固定格式中,在上面 對 middlewares 遍歷之後,將每個中間件第一個參數預置進去,還需要調用兩次,傳入兩個參數才會得到的真正的結果,而在 compose 之後,就只剩下一個參數了,你反應過來了嗎?
chain 之後每個中間件需要一個 (next[上一個中間件的dispatch方法]) 作為第二個參數,而在執行完 compose 之後,多個中間件套在了一起,
當想要執行 middlewareCreator1 的時候,由於 middlewareCreator1 的執行依賴於內部參數的求值,所以會對內部參數進行調用,也就是執行 middlewareCreator2,而 middlewareCreator2 的執行依賴於內部 middlewareCreator3 的執行,所以最終將會先執行 middlewareCreator3 ,middlewareCreator3 傳入了參數 store.dispatch 作為 (next[上一個中間件的dispatch方法]) 會返回一個需要 (action[實際派發的action對象]) 的函數,也就是豐富過 middlewareCreator3 的新的 dispatch 給 middlewareCreator2,這將作為 middlewareCreator2 的第二個參數 (next[上一個中間件的dispatch方法]),然後有了參數的 middlewareCreator2 得以順利調用,這回返回一個需要 (action[實際派發的action對象]) 的函數,也就是豐富過 middlewareCreator3 和 middlewareCreator2 的新 dispatch 給 middlewareCreator1,這將作為 middlewareCreator1 的第二個參數 (next[上一個中間件的dispatch方法]),然後有了參數的 middlewareCreator1 得以順利調用,最後就會返回一個 一個需要 (action[實際派發的action對象]) 的函數,也就是豐富過 middlewareCreator3 、 middlewareCreator2 和 middlewareCreator1 的新 dispatch ,這也就是最終的 dispatch,最後在我們使用的時候,常規調用 dispatch (action)的時候,將第三個參數傳入了進來,進行了第三次調用,返回函數結果。
以上就是整個調用過程。
Middleware3 在接受 store.dispatch 作為 next 參數調用之後會返回一個函數,這個函數需要 action 作為第三個參數,即action => { // .....中間件真正邏輯.... }
而在Middleware2 未得到 Middleware3 的返回結果即未被調用之前 Middleware2 是這樣的
next => action => { // ...一些 Middleware2 代碼... next(action); //...一些 Middleware2 代碼... }
然後在 Middleware3 調用之後, Middleware2 則接受了來自 Middleware3 返回的接受 action 為參數的函數 action => {} 作為 next 參數,然後進行調用,又會返回一個函數傳給在下一個 等待 接受action為參數的函數 action => {} 作為下一個中間件的 next 參數,就這樣層層組裝,豐富了我們的 dispatch。
這個過程再想一下,其實一開始 通過 compose 的第一次調用依次嵌套起來,然後又通過第二次調用,將嵌套的函數從內到外反向調用,生成一個新的 dispatch,而這裏面就用到了兩次 柯裏化和箭頭函數的結合,第一次在 生成chain和調用 compose 方法,第二次在中間件本身(const reduxMiddleware = ({dispatch, getState}[簡化的store]) => (next[上一個中間件的dispatch方法]) => (action[實際派發的action對象]) => {})。
而實現這樣的邏輯的核心代碼,就兩行。
學習並感謝:
JS中的柯裏化(currying):http://www.zhangxinxu.com/wordpress/2013/02/js-currying/
箭頭函數MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions?? (很詳細,值得看)
高階箭頭函數:https://cnodejs.org/topic/56a1d827cd415452622eed07
【 js 基礎 】【 源碼學習 】柯裏化和箭頭函數