函數式編程在前端已經成爲了一個很是熱門的話題。在最近幾年裏,咱們看到很是多的應用程序代碼庫裏大量使用着函數式編程思想。javascript
本文將略去那些晦澀難懂的概念介紹,重點展現在 JavaScript 中到底什麼是函數式的代碼、聲明式與命令式代碼的區別、以及常見的函數式模型都有哪些?想閱讀更多優質文章請猛戳GitHub博客html
函數式編程是一種編程範式,主要是利用函數把運算過程封裝起來,經過組合各類函數來計算結果。函數式編程意味着你能夠在更短的時間內編寫具備更少錯誤的代碼。舉個簡單的例子,假設咱們要把字符串 functional programming is great
變成每一個單詞首字母大寫,咱們能夠這樣實現:前端
var string = 'functional programming is great'; var result = string .split(' ') .map(v => v.slice(0, 1).toUpperCase() + v.slice(1)) .join(' ');
上面的例子先用 split 把字符串轉換數組,而後再經過 map 把各元素的首字母轉換成大寫,最後經過 join 把數組轉換成字符串。 整個過程就是 join(map(split(str)))
,體現了函數式編程的核心思想: 經過函數對數據進行轉換。java
由此咱們能夠獲得,函數式編程有兩個基本特色:git
//命令式 var CEOs = []; for(var i = 0; i < companies.length; i++){ CEOs.push(companies[i].CEO) } //聲明式 var CEOs = companies.map(c => c.CEO);
從上面的例子中,咱們能夠看到聲明式的寫法是一個表達式,無需關心如何進行計數器迭代,返回的數組如何收集,它指明的是作什麼,而不是怎麼作。函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無反作用的純函數,咱們徹底能夠不考慮函數內部是如何實現的,專一於編寫業務代碼。github
指調用函數時不會修改外部狀態,即一個函數調用 n 次後依然返回一樣的結果。編程
var a = 1; // 含有反作用,它修改了外部變量 a // 屢次調用結果不同 function test1() { a++ return a; } // 無反作用,沒有修改外部狀態 // 屢次調用結果同樣 function test2(a) { return a + 1; }
指一個函數只會用到傳遞給它的變量以及本身內部建立的變量,不會使用到其餘變量。數組
var a = 1; var b = 2; // 函數內部使用的變量並不屬於它的做用域 function test1() { return a + b; } // 函數內部使用的變量是顯式傳遞進去的 function test2(a, b) { return a + b; }
指的是一個變量一旦建立後,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程能夠同時訪問同一個不可變變量,讓並行變得更容易實現。 因爲 JavaScript 原生不支持不可變變量,須要經過第三方庫來實現。 (如 Immutable.js,Mori 等等)promise
var obj = Immutable({ a: 1 }); var obj2 = obj.set('a', 2); console.log(obj); // Immutable({ a: 1 }) console.log(obj2); // Immutable({ a: 2 })
咱們常說函數是JavaScript的"第一等公民",指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。下文將要介紹的閉包、高階函數、函數柯里化和函數組合都是圍繞這一特性的應用緩存
若是一個函數引用了自由變量,那麼該函數就是一個閉包。何謂自由變量?自由變量是指不屬於該函數做用域的變量(全部全局變量都是自由變量,嚴格來講引用了全局變量的函數都是閉包,但這種閉包並無什麼用,一般狀況下咱們說的閉包是指函數內部的函數)。
閉包的造成條件:
閉包的用途:
能夠定義一些做用域侷限的持久化變量,這些變量能夠用來作緩存或者計算的中間量等。
// 簡單的緩存工具 // 匿名函數創造了一個閉包 const cache = (function() { const store = {}; return { get(key) { return store[key]; }, set(key, val) { store[key] = val; } } }()); console.log(cache) //{get: ƒ, set: ƒ} cache.set('a', 1); cache.get('a'); // 1
上面例子是一個簡單的緩存工具的實現,匿名函數創造了一個閉包,使得 store 對象 ,一直能夠被引用,不會被回收。
閉包的弊端:持久化變量不會被正常釋放,持續佔用內存空間,很容易形成內存浪費,因此通常須要一些額外手動的清理機制。
函數式編程傾向於複用一組通用的函數功能來處理數據,它經過使用高階函數來實現。高階函數指的是一個函數以函數爲參數,或以函數爲返回值,或者既以函數爲參數又以函數爲返回值。
高階函數常常用於:
JavaScript 語言是原生支持高階函數的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內置的一些高階函數,使用高階函數會讓咱們的代碼更清晰簡潔。
map() 方法建立一個新數組,其結果是該數組中的每一個元素都調用一個提供的函數後返回的結果。map 不會改變原數組。
假設咱們有一個包含名稱和種類屬性的對象數組,咱們想要這個數組中全部名稱屬性放在一個新數組中,如何實現呢?
// 不使用高階函數 var animals = [ { name: "Fluffykins", species: "rabbit" }, { name: "Caro", species: "dog" }, { name: "Hamilton", species: "dog" }, { name: "Harold", species: "fish" }, { name: "Ursula", species: "cat" }, { name: "Jimmy", species: "fish" } ]; var names = []; for (let i = 0; i < animals.length; i++) { names.push(animals[i].name); } console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
// 使用高階函數 var animals = [ { name: "Fluffykins", species: "rabbit" }, { name: "Caro", species: "dog" }, { name: "Hamilton", species: "dog" }, { name: "Harold", species: "fish" }, { name: "Ursula", species: "cat" }, { name: "Jimmy", species: "fish" } ]; var names = animals.map(x=>x.name); console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]
filter() 方法會建立一個新數組,其中包含全部經過回調函數測試的元素。filter 爲數組中的每一個元素調用一次 callback 函數, callback 函數返回 true 表示該元素經過測試,保留該元素,false 則不保留。filter 不會改變原數組,它返回過濾後的新數組。
假設咱們有一個包含名稱和種類屬性的對象數組。 咱們想要建立一個只包含狗(species: "dog")的數組。如何實現呢?
// 不使用高階函數 var animals = [ { name: "Fluffykins", species: "rabbit" }, { name: "Caro", species: "dog" }, { name: "Hamilton", species: "dog" }, { name: "Harold", species: "fish" }, { name: "Ursula", species: "cat" }, { name: "Jimmy", species: "fish" } ]; var dogs = []; for (var i = 0; i < animals.length; i++) { if (animals[i].species === "dog") dogs.push(animals[i]); } console.log(dogs);
// 使用高階函數 var animals = [ { name: "Fluffykins", species: "rabbit" }, { name: "Caro", species: "dog" }, { name: "Hamilton", species: "dog" }, { name: "Harold", species: "fish" }, { name: "Ursula", species: "cat" }, { name: "Jimmy", species: "fish" } ]; var dogs = animals.filter(x => x.species === "dog"); console.log(dogs); // {name: "Caro", species: "dog"} // { name: "Hamilton", species: "dog" }
reduce 方法對調用數組的每一個元素執行回調函數,最後生成一個單一的值並返回。 reduce 方法接受兩個參數:1)reducer 函數(回調),2)一個可選的 initialValue。
假設咱們要對一個數組的求和:
// 不使用高階函數 const arr = [5, 7, 1, 8, 4]; let sum = 0; for (let i = 0; i < arr.length; i++) { sum = sum + arr[i]; } console.log(sum);//25
// 使用高階函數 const arr = [5, 7, 1, 8, 4]; const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0); console.log(sum)//25
咱們能夠經過下圖,形象生動展現三者的區別:
柯里化又稱部分求值,柯里化函數會接收一些參數,而後不會當即求值,而是繼續返回一個新函數,將傳入的參數經過閉包的形式保存,等到被真正求值的時候,再一次性把全部傳入的參數進行求值。
// 普通函數 function add(x,y){ return x + y; } add(1,2); // 3 // 函數柯里化 var add = function(x) { return function(y) { return x + y; }; }; var increment = add(1); increment(2);// 3
這裏咱們定義了一個 add 函數,它接受一個參數並返回一個新的函數。調用 add 以後,返回的函數就經過閉包的方式記住了 add 的第一個參數。那麼,咱們如何來實現一個簡易的柯里化函數呢?
function curryIt(fn) { // 參數fn函數的參數個數 var n = fn.length; var args = []; return function(arg) { args.push(arg); if (args.length < n) { return arguments.callee; // 返回這個函數的引用 } else { return fn.apply(this, args); } }; } function add(a, b, c) { return [a, b, c]; } var c = curryIt(add); var c1 = c(1); var c2 = c1(2); var c3 = c2(3); console.log(c3); //[1, 2, 3]
由此咱們能夠看出,柯里化是一種「預加載」函數的方法,經過傳遞較少的參數,獲得一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的「緩存」,是一種很是高效的編寫函數的方法!
前面提到過,函數式編程的一個特色是經過串聯函數來求值。然而,隨着串聯函數數量的增多,代碼的可讀性就會不斷降低。函數組合就是用來解決這個問題的方法。
假設有一個 compose 函數,它能夠接受多個函數做爲參數,而後返回一個新的函數。當咱們爲這個新函數傳遞參數時,該參數就會「流」過其中的函數,最後返回結果。
//兩個函數的組合 var compose = function(f, g) { return function(x) { return f(g(x)); }; }; //或者 var compose = (f, g) => (x => f(g(x))); var add1 = x => x + 1; var mul5 = x => x * 5; compose(mul5, add1)(2);// =>15
給你們推薦一個好用的BUG監控工具Fundebug,歡迎免費試用!
歡迎關注公衆號:前端工匠,你的成長咱們一塊兒見證!