您好,登錄后才能下訂單哦!
本篇內容介紹了“JS組合函數實例分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
函數編程就像拼樂高!
樂高有各式各樣的零部件,我們將它們組裝拼接,拼成一個更大的組件或模型。
函數編程也有各種功能的函數,我們將它們組裝拼接,用于實現某個特定的功能。
下面來看一個例子,比如我們要使用這兩個函數來分析文本字符串:
function words(str) { return String( str ) .toLowerCase() .split( /\s|\b/ ) .filter( function alpha(v){ return /^[\w]+$/.test( v ); } ); } function unique(list) { var uniqList = []; for (let i = 0; i < list.length; i++) { if (uniqList.indexOf( list[i] ) === -1 ) { uniqList.push( list[i] ); } } return uniqList; } var text = "To compose two functions together"; var wordsFound = words( text ); var wordsUsed = unique( wordsFound ); wordsUsed; // ["to", "compose", "two", "functions", "together"]
不用細看,只用知道:我們先用 words 函數處理了 text,然后用 unique 函數處理了上一處理的結果 wordsFound;
這樣的過程就好比生產線上加工商品,流水線加工。
想象一下,如果你是工廠老板,還會怎樣優化流程、節約成本?
這里作者給了一種解決方式:去掉傳送帶!
即減少中間變量,我們可以這樣調用:
var wordsUsed = unique( words( text ) ); wordsUsed
確實,少了中間變量,更加清晰,還能再優化嗎?
我們還可以進一步把整個處理流程封裝到一個函數內:
function uniqueWords(str) { return unique( words( str ) ); } uniqueWords(text)
這樣就像是一個黑盒,無需管里面的流程,只用知道這個盒子輸入是什么!輸出是什么!輸入輸出清晰,功能清晰,非常“干凈”!如圖:
與此同時,它還能被搬來搬去,或再繼續組裝。
我們回到 uniqueWords() 函數的內部,它的數據流也是清晰的:
uniqueWords <-- unique <-- words <-- text
上面的封裝 uniqueWords 盒子很 nice ,如果要不斷的封裝像 uniqueWords 的盒子,我們要一個一個的去寫嗎?
function uniqueWords(str) { return unique( words( str ) ); } function uniqueWords_A(str) { return unique_A( words_A( str ) ); } function uniqueWords_B(str) { return unique_B( words_B( str ) ); } ...
所以,一切為了偷懶,我們可以寫一個功能更加強大的函數來實現自動封裝盒子:
function compose2(fn2,fn1) { return function composed(origValue){ return fn2( fn1( origValue ) ); }; } // ES6 箭頭函數形式寫法 var compose2 = (fn2,fn1) => origValue => fn2( fn1( origValue ) );
接著,調用就變成了這樣:
var uniqueWords = compose2( unique, words ); var uniqueWords_A = compose2( unique_A, words_A ); var uniqueWords_B = compose2( unique_B, words_B );
太清晰了!
上面,我們組合了兩個函數,實際上我們也可以組合 N 個函數;
finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue
比如用一個 compose 函數來實現(敲重點):
function compose(...fns) { return function composed(result){ // 拷貝一份保存函數的數組 var list = fns.slice(); while (list.length > 0) { // 將最后一個函數從列表尾部拿出 // 并執行它 result = list.pop()( result ); } return result; }; } // ES6 箭頭函數形式寫法 var compose = (...fns) => result => { var list = fns.slice(); while (list.length > 0) { // 將最后一個函數從列表尾部拿出 // 并執行它 result = list.pop()( result ); } return result; };
基于前面 uniqueWords(..) 的例子,我們進一步再增加一個函數來處理(過濾掉長度小于等于4的字符串):
function skipShortWords(list) { var filteredList = []; for (let i = 0; i < list.length; i++) { if (list[i].length > 4) { filteredList.push( list[i] ); } } return filteredList; } var text = "To compose two functions together"; var biggerWords = compose( skipShortWords, unique, words ); var wordsUsed = biggerWords( text ); wordsUsed; // ["compose", "functions", "together"]
這樣 compose 函數就有三個入參且都是函數了。我們還可以利用偏函數的特性實現更多:
function skipLongWords(list) { /* .. */ } var filterWords = partialRight( compose, unique, words ); // 固定 unique 函數 和 words 函數 var biggerWords = filterWords( skipShortWords ); var shorterWords = filterWords( skipLongWords ); biggerWords( text ); shorterWords( text );
filterWords 函數是一個更具有特定功能的變體(根據第一個函數的功能來過濾字符串)。
compose(..)函數非常重要,但我們可能不會在生產中使用自己寫的 compose(..),而更傾向于使用某個庫所提供的方案。了解其底層工作的原理,對我們強化理解函數式編程也非常有用。
我們理解下 compose(..) 的另一種變體 —— 遞歸的方式實現:
function compose(...fns) { // 拿出最后兩個參數 var [ fn1, fn2, ...rest ] = fns.reverse(); var composedFn = function composed(...args){ return fn2( fn1( ...args ) ); }; if (rest.length == 0) return composedFn; return compose( ...rest.reverse(), composedFn ); } // ES6 箭頭函數形式寫法 var compose = (...fns) => { // 拿出最后兩個參數 var [ fn1, fn2, ...rest ] = fns.reverse(); var composedFn = (...args) => fn2( fn1( ...args ) ); if (rest.length == 0) return composedFn; return compose( ...rest.reverse(), composedFn ); };
通過遞歸進行重復的動作比在循環中跟蹤運行結果更易懂,這可能需要更多時間去體會;
基于之前的例子,如果我們想讓參數反轉:
var biggerWords = compose( skipShortWords, unique, words ); // 變成 var biggerWords = pipe( words, unique, skipShortWords );
只需要更改 compose(..) 內部實現這一句就行:
... while (list.length > 0) { // 從列表中取第一個函數并執行 result = list.shift()( result ); } ...
雖然只是顛倒參數順序,這二者沒有本質上的區別。
你是否會疑問:什么情況下可以封裝成上述的“盒子”呢?
這就很考驗 —— 抽象的能力了!
實際上,有兩個或多個任務存在公共部分,我們就可以進行封裝了。
比如:
function saveComment(txt) { if (txt != "") { comments[comments.length] = txt; } } function trackEvent(evt) { if (evt.name !== undefined) { events[evt.name] = evt; } }
就可以抽象封裝為:
function storeData(store,location,value) { store[location] = value; } function saveComment(txt) { if (txt != "") { storeData( comments, comments.length, txt ); } } function trackEvent(evt) { if (evt.name !== undefined) { storeData( events, evt.name, evt ); } }
在做這類抽象時,有一個原則是,通常被稱作 DRY(don't repeat yourself),即便我們要花時間做這些非必要的工作。
抽象能讓你的代碼走得更遠! 比如上例,還能進一步升級:
function conditionallyStoreData(store,location,value,checkFn) { if (checkFn( value, store, location )) { store[location] = value; } } function notEmpty(val) { return val != ""; } function isUndefined(val) { return val === undefined; } function isPropUndefined(val,obj,prop) { return isUndefined( obj[prop] ); } function saveComment(txt) { conditionallyStoreData( comments, comments.length, txt, notEmpty ); } function trackEvent(evt) { conditionallyStoreData( events, evt.name, evt, isPropUndefined ); }
這樣 if 語句也被抽象封裝了。
抽象是一個過程,程序員將一個名字與潛在的復雜程序片段關聯起來,這樣該名字就能夠被認為代表函數的目的,而不是代表函數如何實現的。通過隱藏無關的細節,抽象降低了概念復雜度,讓程序員在任意時間都可以集中注意力在程序內容中的可維護子集上。—— 《程序設計語言》
我們在本系列初始提到:“一切為了創造更可讀、更易理解的代碼。”
從另一個角度,抽象就是將命令式代碼變成聲命式代碼的過程。從“怎么做”轉化成“是什么”。
命令式代碼主要關心的是描述怎么做來準確完成一項任務。聲明式代碼則是描述輸出應該是什么,并將具體實現交給其它部分。
比如 ES6 增加的結構語法:
function getData() { return [1,2,3,4,5]; } // 命令式 var tmp = getData(); var a = tmp[0]; var b = tmp[3]; // 聲明式 var [ a ,,, b ] = getData();
“JS組合函數實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。