3C科技 娛樂遊戲 美食旅遊 時尚美妝 親子育兒 生活休閒 金融理財 健康運動 寰宇綜合

Zi 字媒體

2017-07-25T20:27:27+00:00
加入好友
目前前端界(以及其他一些領域)對函數式編程大體上兩種態度,一些人是覺得函數式編程特牛逼,尤其是現在許多新生的框架和庫都在標榜自己的函數式特徵。而另一些人,又覺得函數式編程學起來很難,而且似乎也沒有什麼卵用,理由是在自己經歷的項目裡面很難看到具體的函數式編程應用場景,甚至其中許多人認同一個觀點,覺得函數式編程只適合於學術研究,很難在工程項目中實際使用。不管你在閱讀本文之前屬於哪一種人,又或者你是剛接觸函數式編程的新人,都沒有關係。本文不是研究函數式編程範式的學術研究,而函數式編程作為一個可以說是程序設計理論中最古老的編程範式,在它幾十年上百年的發展歷史中,已經積累了大量的資料和素材,對於想要在學術領域裡完全弄明白它的同學,完全可以在網上、書店里找到各種資料。本文的重點不在於概念,而在於實戰。因此,你不會聽到太多各種函數式編程的名詞討論,比如諸如 Curry、Mond 之類的專業術語。相反,我們主要來討論函數式編程在前端領域內使用的一些實際例子,了解為什麼前端需要學習函數式編程,使用函數式編程寫代碼能給我們帶來什麼。如果弄明白了這些,那麼關於函數式編程不實用的謠言也就不攻自破了。數據抽象或過程抽象為什麼我們接受面向過程或面向對象思想很容易,而我們要完全接受函數式編程卻感覺難得多?我認為這個問題大體上可以這麼解釋:人腦本能地容易理解「看得見「、「摸得著」的物體,對於「運動」和「變化」一類不著形的東西,人腦理解起來要略微地費勁一些。而人類要做好一件複雜的事情,大腦有兩種抽象方向,一種是對實體進行抽象,另一種是對過程進行抽象:簡答來說,即在軟體設計的過程中,如果要保證軟體產品的功能穩定可用,同時要保證它的靈活性和可擴展性,那麼系統就要有變化的部分和不變的部分。哪些部分應當設計成「不變」,哪些部分應當設計成「可變」,在這個取捨過程中,FP(函數式編程)和 OOP(面向對象編程)正是走了兩條不同的路線。面向對象對數據進行抽象,將行為以對象方法的方式封裝到數據實體內部,從而降低系統的耦合度。而函數式編程,選擇對過程進行抽象,將數據以輸入輸出流的方式封裝進過程內部,從而也降低系統的耦合度。兩者雖是截然不同,然而在系統設計的目標上可以說是殊途同歸的。面向對象思想和函數式編程思想也是不矛盾的,因為一個龐大的系統,可能既要對數據進行抽象,又要對過程進行抽象,或者一個局部適合進行數據抽象,另一個局部適合進行過程抽象,這都是可能的。數據抽象不一定以對象實體為形式,同樣過程抽象也不是說形式上必然是 functional 的,比如流式對象(InputStream、OutputStream)、Express 的 middleware,就帶有明顯的過程抽象的特徵。但是在通常情況下,OOP更適合用來做數據抽象,FP更適合用來做過程抽象。純函數再具體深入下去之前,我們先來解答一個問題,那就是為什麼用 FP 或過程抽象能夠降低系統的耦合度。這裡我們要先理解一個概念,這個概念叫「純函數」。根據定義,如果一個函數符合兩個條件,它被稱為純函數:此函數在相同的輸入值時,總是產生相同的輸出。函數的輸出和當前運行環境的上下文狀態無關。此函數運行過程不影響運行環境,比如不會觸發事件、更改環境中的對象、終端輸出值等。簡單來說,也就是當一個函數的輸出不受外部環境影響,同時也不影響外部環境時,該函數就是純函數。JavaScript 內置函數中有不少純函數,也有不少非純函數。比如以下函數是純函數:String.prototype.toUpperCaseArray.prototype.mapFunction.prototype.bind以下函數不是純函數:Math.randomDate.nowdocument.body.appendChildArray.prototype.sort為什麼要區分純函數和非純函數呢?因為在系統里,純函數與非純函數相比,在可測試性、可維護性、可移植性、并行計算和可擴展性方面都有著巨大的優勢。在這裡我用可測試性來舉例:對於純函數,因為是無狀態的,測試的時候不需要構建運行時環境,也不需要用特定的順序進行測試:test(t = { t.is(add(10 20) 30); //add(x,y) 是個純函數,不需要為它構建測試環境 ...});對於非純函數,就比較複雜:test.before(t = { let list = document.createElement('ul'); list.id = 'xxxxxx'; ...});test(t = { let list = document.getElementById('xxxxxx'); t.is(sortList(list).innerHTML `<ul> ... </ul>`);});test.after(t = { ... document.removeChild(list);});函數式編程能夠減少系統中的非純函數首先我們看一個例子://two impure functionsfunction setColor(el color){ el.style.color = color;}function setColors(els color){ els.forEach(el = setColor(el color));}let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');setColors(items2 'green');setColors(items1 'red');在這裡我們有兩個彼此依賴的非純函數,setColor(el, color) 和 setColors(els, color)。在測試的時候,我們需要構建環境來測試兩個函數。現在,我們用函數式編程思想來改造這個系統://only one impure functionfunction batch(fn){ return function(target ...args){ if(target.length >= 0){ return Array.from(target).map(item = fn.apply(this [item ...args])); }else{ return fn.apply(this [target ...args]); } }}function setColor(el color){ el.style.color = color;}let setColors = batch(setColor);let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');setColors(items2 'green');setColors(items1 'red');在這裡,我們建立一個過程抽象的高階函數 batch(fn),這個函數的作用是,對它的輸入函數返回一個新的函數,這個函數與輸入函數的區別是,如果調用的第一個實參是一個數組,那麼將這個數組展開,用每一個值依次調用輸入函數,返回一個數組,包活每次調用返回的結果。batch(fn) 本身雖然看似複雜,但是有意思的事,這個函數無疑是純函數,所以 batch(fn) 自身的測試是非常簡單的:test(t = { let add = (x y) = x + y; let listAdd = batch(add); t.deepEqual(listAdd([123] 1) [234]);});由於我們上面舉的例子 setColor 和 setColors 雖然不是純函數,但是卻非常簡單,因此似乎設計 batch(fn) 的意義不大,有把系統變得更複雜的嫌疑。然而,對於有許多操作 DOM 的函數的框架或庫,有了 batch(fn),我們就可以實現很簡單的介面(對單一元素操作),然後利用 batch(fn) 獲得更複雜介面(對元素進行批量操作),從而大大降低系統本身的複雜的,提升可維護性。注意一點,batch(fn) 輸出的函數有副作用,然而 batch(fn) 用閉包將輸出的函數的副作用限制在了 batch(fn) 的作用域內。Ramda.js 的 lift 方法Ramda.js 的 lift 方法和 batch 有一點點類似,不過功能更強大。讓我們來用它實現一個有一點點「燒腦」的效果,來作為這篇文章的結尾:async function reducer(promise action){ let res = await promise; return action(res);}function continuous(...functors){ return async function(input){ return await functors.reduce(reducer input) }}function sleep(ms){ return new Promise(resolve = setTimeout(resolve ms));}async function setColor(item color){ await sleep(500); item.style.color = color;}let comb = R.lift((el color) = { return [el color];});let changeColorTo = (args) = R.partial(setColor args);let items = Array.from(list.children);let task = R.map(changeColorTo comb( items ['red' 'orange' 'yellow']));continuous(...task)(0);

本文由yidianzixun提供 原文連結

寫了 5860316篇文章,獲得 23313次喜歡
精彩推薦