javascript函數式編程(一)

1、引言javascript

函數式編程的歷史已經很悠久了,可是最近幾年卻頻繁的出如今大衆的視野,不少不支持函數式編程的語言也在積極加入閉包,匿名函數等很是典型的函數式編程特性。大量的前端框架也標榜本身使用了函數式編程的特性,好像一旦跟函數式編程沾邊,就很高大上同樣,並且還有一些專門針對函數式編程的框架和庫,好比:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS等。函數式編程變得愈來愈流行,掌握這種編程範式對書寫高質量和易於維護的代碼都大有好處,因此咱們有必要掌握它。html

2、什麼是函數式編程前端

維基百科定義:java

函數式編程(英語:functional programming),又稱泛函編程,是一種編程範式,它將電腦運算視爲數學上的函數計算,而且避免使用程序狀態以及易變對象。程序員

3、純函數(函數式編程的基石,無反作用的函數)es6

在初中數學裏,函數f的定義是:對於輸入x產生一個惟一輸出y=f(x)。這即是純函數。它符合兩個條件:數據庫

1.此函數在相同的輸入值時,老是產生相同的輸出。函數的輸出和當前運行環境的上下文狀態無關。編程

2.此函數運行過程不影響運行環境,也就是無反作用(如觸發事件、發起http請求、打印/log等)。json

簡單來講,也就是當一個函數的輸出不受外部環境影響,同時也不影響外部環境時,該函數就是純函數,也就是它只關注邏輯運算和數學運算,同一個輸入總獲得同一個輸出。api

javascript內置函數有很多純函數,也有很多非純函數。

純函數:

Array.prototype.slice

Array.prototype.map

String.prototype.toUpperCase

非純函數:

Math.random

Date.now

Array.ptototype.splice

這裏咱們以slice和splice方法舉例:

var xs = [1,2,3,4,5];
// 純的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]

// 不純的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []

咱們看到調用數組的slice方法每次返回的結果徹底相同,同時xs不會被改變,而調用splice方法每次返回值都不同,同時xs變得面目全非。

這就是咱們強調使用純函數的緣由,由於純函數相對於非純函數來講,在可緩存性、可移植性、可測試性以及並行計算方面都有着巨大的優點。

這裏咱們以可緩存性舉例:

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是經過硬編碼的方式定義的,這限制了函數的擴展性,咱們能夠在後面的柯里化中看到如何優雅的使用函數式解決這個問題。因此把一個函數變純的基本手段是不要依賴系統狀態。

4、函數柯里化

 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);
// =>true

這代表函數柯里化是一種「預加載」函數的能力,經過傳遞一到兩個參數調用函數,就能獲得一個記住了這些參數的新函數。從某種意義上來說,這是一種對參數的緩存,是一種很是高效的編寫函數的方法:

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"]

5、函數組合

假設咱們須要對一個字符串作一些列操做,以下,爲了方便舉例,咱們只對一個字符串作兩種操做,咱們定義了一個新函數shout,先調用toUpperCase,而後把返回值傳給exclaim函數,這樣作有什麼很差呢?

不優雅,若是作得事情一多,嵌套的函數會很是深,並且代碼是由內往外執行,不直觀,咱們但願代碼從右往左執行,這個時候咱們就得使用組合。

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };

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'

 6、聲明式和命令式代碼

命令式代碼:命令「機器」如何去作事情(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 函數,這麼一來咱們主要的業務代碼就無須改動了。

函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無反作用的純函數,咱們徹底能夠不考慮函數內部是如何實現的,專一於編寫業務代碼。優化代碼時,目光只須要集中在這些穩定堅固的函數內部便可。

相反,不純的不函數式的代碼會產生反作用或者依賴外部系統環境,使用它們的時候老是要考慮這些不乾淨的反作用。在複雜的系統中,這對於程序員的心智來講是極大的負擔。

7、Point Free

pointfree 模式指的是,永遠沒必要說出你的數據。它的意思是說,函數無須說起將要操做的數據是什麼樣的。一等公民的函數、柯里化(curry)以及組合協做起來很是有助於實現這種模式。

// 非 pointfree,由於提到了數據:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

這種風格可以幫助咱們減小沒必要要的命名,讓代碼保持簡潔和通用。固然,爲了在一些函數中寫出Point Free的風格,在代碼的其它地方必然是不那麼Point Free的,這個地方須要本身取捨。

8、示例應用

擁有了以上的知識,咱們是時候該寫一個示例應用了。

這裏咱們使用了 ramda ,沒有用 lodash 或者其餘類庫。ramda 提供了 compose、curry 等不少函數。

咱們的應用將作四件事:

1.根據特定搜索關鍵字構造 url
2.向 flickr 發送 api 請求
3.把返回的 json 轉爲 html 圖片
4.把圖片放到屏幕上

上面提到了兩個不純的動做,即從 flickr 的 api 獲取數據和在屏幕上放置圖片這兩件事。咱們先來定義這兩個動做,這樣就能隔離它們了。這裏咱們只是簡單包裝了一下jQuery的getJSON函數,把它變爲一個 curry 函數,還有就是把參數位置也調換了下,咱們把它們放在 Impure 命名空間下以用來隔離,這樣咱們就知道它們都是危險函數。

運用函數柯里化和函數組合的技巧,咱們就能夠建立一個函數式的實際應用了:

 1 var _ = R;
 2 var Impure = {
 3       getJSON: _.curry(function(callback, url) {
 4         $.getJSON(url, callback);
 5       }),
 6 
 7       setHtml: _.curry(function(sel, html) {
 8         $(sel).html(html);
 9       })
10     };
11 
12     var img = function (url) {
13       return $('<img />', { src: url });
14     };
15 
16     ////////////////////////////////////////////
17 
18     var url = function (t) {
19       return '你的接口地址';
20     };
21 
22     var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
23 
24     var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
25 
26     var images = _.compose(_.map(img), srcs);
27 
28     var renderImages = _.compose(Impure.setHtml("body"), images);
29 
30     var app = _.compose(Impure.getJSON(renderImages), url);
31 
32     app("cats");

 

看看,多麼美妙的聲明式規範啊,只說作什麼,不說怎麼作。如今咱們能夠把每一行代碼都視做一個等式,變量名所表明的屬性就是等式的含義。

9、總結

咱們已經見識到如何在一個小而不失真實的應用中運用新技能了,可是異常處理以及代碼分支呢?如何讓整個應用都是函數式的,而不只僅是把破壞性的函數放到命名空間下?如何讓應用更安全更富有表現力?

我會在下一篇文章中(JavaScript函數式編程(二))介紹函數式編程的更加高階一些的知識,例如Functor、Monad、Applicative等概念。

相關文章
相關標籤/搜索