javascript函數語言程式設計基礎
目錄
- 一、引言
- 二、什麼是函式式
- 三、純函式(函數語言程式設計的基石,無副作用的函式)
- 四、函式柯里化
- 五、函式組合
- 六、宣告式和命令式程式碼
- 七、Point Free
- 八、示例應用
- 九、總結
一、引言
函數語言程式設計的歷史已經很悠久了,但是最近幾年卻頻繁的出現在大眾的視野,很多不支援函數語言程式設計的語言也在積極加入閉包,匿名函式等非常典型的函數語言程式設計特性。大量的前端框架也標榜自己使用了函數語言程式設計的特性,好像一旦跟函數語言程式設計沾邊,就很高大上一樣,而且還有一些專門針對函數語言程式設計的框架和庫,比如:Rx、cycleJS、ramdaJS、lodashJS、underscoreJS 等。函數語言程式設計變得越來越流行,掌握這種程式設計正規化對書寫高質量和易於維護的程式碼都大有好處,所以我們有必要掌握它。
二、什麼是函數語言程式設計
維基百科定義:函數語言程式設計(英語:functional programming),又稱泛函程式設計,是一種程式設計正規化,它將電腦運算視為數學上的函式計算,並且避免使用程式狀態以及易變物件。
三、純函式(函數語言程式設計的基石,無副作用的函式)
在初中數學裡,函式 f 的定義是:對於輸入 x 產生一個唯一輸出 y=f(x)。這便是純函式。它符合兩個條件:1.此函式在相同的輸入值時,總是產生相同的輸出。函式的輸出和當前執行環境的上下文狀態無關。2.此函式執行過程不影響執行環境,也就是無副作用(如觸發事件、發起 http 請求、列印/log 等)。簡單來說,也就是當一個函式的輸出不受外部環境影響,同時也不影響外部環境時,該函式就是純函式,也就是它只關注邏輯運算和數學運算,同一個輸入總得到同一個輸出。 內建函式有不少純函式,也有不少非純函式。
純函式:Array.prototype.sliceArray.prototype.mapString.prototype.toUpperCase
非純函式:Math.randomDate.nowArray.ptototype.splice
這裡我們以 slice 和 splice 方法舉例:
var xs = [1,2,3,4,5]; // 純的 xs.slice(0,3); //=> [1,3] xs.slice(0,3] // 不純的 xs.splice(0,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []
我們看到呼叫陣列的 slice 方法每baneR
var squareNumber = memoize(function(x){ return x*x; }); squareNumber(4); //=> 16 squareNumber(4); // 從快取中讀取輸入值為 4 的結果 //=> 16
那我們如何把一個非純函式變純呢?比如下面這個函式:
var minimum = 21; var checkAge = function(age) { return age >= minimum; };
這個函式的返回值依賴於可變變數 minimum 的值,它依賴於系統狀態。在大型系統中,這種對於外部狀態的依賴是造成系統複雜性大大提高的主要原因。
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
通過改造,我們把 checkAge 變成了一個純函式,它不依賴於系統狀態,但是 minimum 是通過硬編碼的方式定義的,這限制了函式的擴充套件性,我們可以在後面的柯里化中看到如何優雅的使用函式式解決這個問題。所以把一個函式變純的基本手段是不要依賴系統狀態。
四、函式柯里化
curry 的概念很簡單:將一個低階函式轉換為高階函式的過程就叫柯里化。
用一個形象的比喻就是:
比如對於加法操作:var add = (x,y) => x + y,我們可以這樣柯里化:
//es5寫法 var add = function(x) { return function(y) { return x + y; }; }; //es6寫法 var add = x => (y => x + y); //試試看 var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12
對於加法這種極其簡單的函式來說,柯里化並沒有什麼用。還記得上面的 checkAge 函式嗎?我們可以這樣柯里化它:
var checkage = min => (age => age > min); var checkage18 = checkage(18); checkage18(20); // =>truehttp://www.cppcns.com
這表明函式柯里化是一種“預載入”函式的能力,通過傳遞一到兩個引數呼叫函式,就能得到一個記住了這些引數的新函式。從某種意義上來講,這是一種對引數的快取,是一種非常高效的編寫函式的方法:
var curry = require('lodash').curry; //柯里化兩個純函式 var match = curry((what,str) => str.match(what)); var filter = curry((f,ary) => ary.filter(f)); //判斷字串裡有沒有空格 var hasSpaces = match(/\s+/g); hasSpaces("hello world"); // [ ' ' ] hasSpaces("spaceless"); // null var findSpaces = filter(hasSpaces); findSpaces(["tori_spelling","tori amos"]); // ["tori amos"]
五、函式組合
假設我們需要對一個字串做一些列操作,如下,為了方便舉例,我們只對一個字串做兩種操作,我們定義了一個新函式 shout,先呼叫 toUpperCase,然後把返回值傳給 exclaim 函式,這樣做有什麼不好呢?不優雅,如果做得事情一多,巢狀的函式會非常深,而且程式碼是由內往外執行,不直觀,我們希望程式碼從右往左執行,這個時候我們就得使用組合。
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };http://www.cppcns.com
var shout = function(x){
return exclaim(toUpperCase(x));
};
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
使用組合,我們可以這樣定義我們的 shout 函式:
//定義compose var compose = (...args) => x => args.reduceRight((value,item) => item(value),x); var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = compose(exclaim,toUpperCase); shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
程式碼從右往左執行,非常清晰明瞭,一目瞭然。我們定義的 compose 像 N 面膠一樣,可以將任意多個純函式結合到一起。這種靈活的組合可以讓我們像拼積木一樣來組合函式式的程式碼:
var head = function(x) { return x[0]; }; var reverse = reduce(function(acc,x){ return [x].concat(acc); },[]); var last = compose(head,reverse); last(['jumpkick','roundhouse','uppercut']); //=> 'uppercut'
六、宣告式和命令式程式碼
命令式程式碼:命令“機器”如何去做事情(how),這樣不管你想要的是什麼(what),它都會按照你的命令實現。宣告式程式碼:告訴“機器”你想要的是什麼(what),讓機器想出如何去做(how)。與命令式不同,宣告式意味著我們要寫表示式,而不是一步一步的指示。以 SQL 為例,它就沒有“先做這個,再做那個”的命令,有的只是一個指明我們想要從取什麼資料的表示式。至於如何取資料則是由它自己決定的。以後資料庫升級也好,SQL 引擎優化也好,根本不需要更改查詢語句。這是因為,有多種方式解析一個表示式並得到相同的結果。這裡為了方便理解,我們來看一個例子:
// 命令式 var makes = []; for (var i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 宣告式 var makes = cars.map(function(car){ return car.make; });
命令式的迴圈要求你必須先例項化一個數組,而且執行完這個例項化語句之後,直譯器才繼續執行後面的程式碼。然後再直接迭代 cars 列表,手動增加計數器,就像你開了一輛零部件全部暴露在外的汽車一樣。這不是優雅的程式設計師應該做的。宣告式的寫法是一個表示式,如何進行計數器迭代,返回的陣列如何收集,這些細節都隱藏了起來。它指明的是做什麼,而不是怎麼做。除了更加清晰和簡潔之外,map 函式還可以進一步獨立優化,甚至用直譯器內建的速度極快的 map 函式,這麼一來我們主要的業務程式碼就無須改動了。函數語言程式設計的一個明顯的好處就是這種宣告式的程式碼,對於無副作用的純函式,我們完全可以不考慮函式內部是如何實現的,專注於編寫業務程式碼。優化程式碼時,目光只需要集中在這些穩定堅固的函式內部即可。相反,不純的不函式式的程式碼會產生副作用或者依賴外部系統環境,使用它們的時候總是要考慮這些不乾淨的副作用。在複雜的系統中,這對於程式設計師的心智來說是極大的負擔。
七、Point Free
pointfree 模式指的是,永遠不必說出你的資料。它的意思是說,函式無須提及將要操作的資料是什麼樣的。一等公民的函式、柯里化(curry)以及組合協作起來非常有助於實現這種模式。
// 非 pointfree,因為提到了資料:www.cppcns.comword var snakeCase = function (word) { return word.toLowerCase().replace(/\s+/ig,'_'); }; // pointfree var snakeCase = compose(replace(/\s+/ig,'_'),toLowerCase);
這種風格能夠幫助我們減少不必要的命名,讓程式碼保持簡潔和通用。當然,為了在一些函式中寫出 Point Free 的風格,在程式碼的其它地方必然是不那麼 Point Free 的,這個地方需要自己取捨。
八、示例應用
擁有了以上的知識,我們是時候該寫一個示例應用了。這裡我們使用了 ramda ,沒有用 lodash 或者其他類庫。ramda 提供了 compose、curry 等很多函式。我們的應用將做四件事:
1.根據特定搜尋關鍵字構造 url
2.向 flickr 傳送 api 請求
3.把返回的 json 轉為 html 圖片
4.把圖片放到螢幕上上面提到了兩個不純的動作,即從 flickr 的 api 獲取資料和在螢幕上放置圖片這兩件事。我們先來定義這兩個動作,這樣就能隔離它們了。這裡我們只是簡單包裝了一下 的 getJSON 函式,把它變為一個 curry 函式,還有就是把引數位置也調換了下,我們把它們放在 Impure 名稱空間下以用來隔離,這樣我們就知道它們都是危險函式。運用函式柯里化和函式組合的技巧,我們就可以建立一個函式式的實際應用了:
預覽地址:https://code.h5jun.com/vixe/1/edit?html,js,output 看看,多麼美妙的宣告式規範啊,只說做什麼,不說怎麼做。現在我們可以把每一行程式碼都視作一個等式,變數名所代表的屬性就是等式的含義。
九、總結
我們已經見識到如何在一個小而不失真實的應用中運用新技能了,但是異常處理以及程式碼分支呢?如何讓整個應用都是函式式的,而不僅僅是把破壞性的函式放到名稱空間下?如何讓應用更安全更富有表現力?我會在下一篇文章中介紹函數語言程式設計的更加高階一些的知識,例如 Functor、Monad、Applicative 等概念。
以上就是script函數語言程式設計基礎的詳細內容,更多關於javascript函數語言程式設計的資料請關注我們其它相關文章!