'一文帶你瞭解JavaScript 函數式編程'

泛函編程 JavaScript 電腦 心萊科技 2019-08-24
"

前言

函數式編程在前端已經成為了一個非常熱門的話題。在最近幾年裡,我們看到非常多的應用程序代碼庫裡大量使用著函數式編程思想。

本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什麼是函數式的代碼、聲明式與命令式代碼的區別、以及常見的函數式模型都有哪些?更多優質文章請猛戳https://github.com/ljianshu/Blog

一、什麼是函數式編程

函數式編程是一種編程範式,主要是利用函數把運算過程封裝起來,通過組合各種函數來計算結果。函數式編程意味著你可以在更短的時間內編寫具有更少錯誤的代碼。舉個簡單的例子,假設我們要把字符串 functional programmingisgreat變成每個單詞首字母大寫,我們可以這樣實現:

  1. var string = 'functional programming is great';

  2. var result = string

  3. .split(' ')

  4. .map(v => v.slice(0, 1).toUpperCase + v.slice(1))

  5. .join(' ');

上面的例子先用 split 把字符串轉換數組,然後再通過 map 把各元素的首字母轉換成大寫,最後通過 join 把數組轉換成字符串。整個過程就是 join(map(split(str))),體現了函數式編程的核心思想:通過函數對數據進行轉換

由此我們可以得到,函數式編程有兩個基本特點:

  • 通過函數來對數據進行轉換

  • 通過串聯多個函數來求結果

二、對比聲明式與命令式

  • 命令式:我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。命令式代碼中頻繁使用語句,來完成某個行為。比如 for、if、switch、throw 等這些語句。

  • 聲明式:我們通過寫表達式的方式來聲明我們想幹什麼,而不是通過一步一步的指示。表達式通常是某些函數調用的複合、一些值和操作符,用來計算出結果值。

  1. //命令式

  2. var CEOs = ;

  3. for(var i = 0; i < companies.length; i++){

  4. CEOs.push(companies[i].CEO)

  5. }


  6. //聲明式

  7. var CEOs = companies.map(c => c.CEO);

從上面的例子中,我們可以看到聲明式的寫法是一個表達式,無需關心如何進行計數器迭代,返回的數組如何收集,它指明的是做什麼,而不是怎麼做。函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注於編寫業務代碼。

三、常見特性

無副作用

指調用函數時不會修改外部狀態,即一個函數調用 n 次後依然返回同樣的結果。

  1. var a = 1;

  2. // 含有副作用,它修改了外部變量 a

  3. // 多次調用結果不一樣

  4. function test1 {

  5. a++

  6. return a;

  7. }


  8. // 無副作用,沒有修改外部狀態

  9. // 多次調用結果一樣

  10. function test2(a) {

  11. return a + 1;

  12. }

透明引用

指一個函數只會用到傳遞給它的變量以及自己內部創建的變量,不會使用到其他變量。

  1. var a = 1;

  2. var b = 2;

  3. // 函數內部使用的變量並不屬於它的作用域

  4. function test1 {

  5. return a + b;

  6. }

  7. // 函數內部使用的變量是顯式傳遞進去的

  8. function test2(a, b) {

  9. return a + b;

  10. }

不可變變量

指的是一個變量一旦創建後,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程可以同時訪問同一個不可變變量,讓並行變得更容易實現。由於 JavaScript 原生不支持不可變變量,需要通過第三方庫來實現。(如 Immutable.js,Mori 等等)

  1. var obj = Immutable({ a: 1 });

  2. var obj2 = obj.set('a', 2);

  3. console.log(obj); // Immutable({ a: 1 })

  4. console.log(obj2); // Immutable({ a: 2 })

函數是一等公民

我們常說函數是JavaScript的"第一等公民",指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。下文將要介紹的閉包、高階函數、函數柯里化和函數組合都是圍繞這一特性的應用

四、常見的函數式編程模型

1.閉包(Closure)

如果一個函數引用了自由變量,那麼該函數就是一個閉包。何謂自由變量?自由變量是指不屬於該函數作用域的變量(所有全局變量都是自由變量,嚴格來說引用了全局變量的函數都是閉包,但這種閉包並沒有什麼用,通常情況下我們說的閉包是指函數內部的函數)。

閉包的形成條件:

  • 存在內、外兩層函數

  • 內層函數對外層函數的局部變量進行了引用

閉包的用途:可以定義一些作用域侷限的持久化變量,這些變量可以用來做緩存或者計算的中間量等

  1. // 簡單的緩存工具

  2. // 匿名函數創造了一個閉包

  3. const cache = (function {

  4. const store = {};


  5. return {

  6. get(key) {

  7. return store[key];

  8. },

  9. set(key, val) {

  10. store[key] = val;

  11. }

  12. }

  13. });

  14. console.log(cache) //{get: ƒ, set: ƒ}

  15. cache.set('a', 1);

  16. cache.get('a'); // 1

上面例子是一個簡單的緩存工具的實現,匿名函數創造了一個閉包,使得 store 對象 ,一直可以被引用,不會被回收。

閉包的弊端:持久化變量不會被正常釋放,持續佔用內存空間,很容易造成內存浪費,所以一般需要一些額外手動的清理機制。

2.高階函數

函數式編程傾向於複用一組通用的函數功能來處理數據,它通過使用高階函數來實現。高階函數指的是一個函數以函數為參數,或以函數為返回值,或者既以函數為參數又以函數為返回值

高階函數經常用於:

  • 抽象或隔離行為、作用,異步控制流程作為回調函數,promises,monads等

  • 創建可以泛用於各種數據類型的功能

  • 部分應用於函數參數(偏函數應用)或創建一個柯里化的函數,用於複用或函數複合。

  • 接受一個函數列表並返回一些由這個列表中的函數組成的複合函數。

JavaScript 語言是原生支持高階函數的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內置的一些高階函數,使用高階函數會讓我們的代碼更清晰簡潔。

map

map 方法創建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數後返回的結果。map 不會改變原數組。

假設我們有一個包含名稱和種類屬性的對象數組,我們想要這個數組中所有名稱屬性放在一個新數組中,如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = ;

  11. for (let i = 0; i < animals.length; i++) {

  12. names.push(animals[i].name);

  13. }

  14. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = animals.map(x=>x.name);

  11. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

filter

filter 方法會創建一個新數組,其中包含所有通過回調函數測試的元素。filter 為數組中的每個元素調用一次 callback 函數, callback 函數返回 true 表示該元素通過測試,保留該元素,false 則不保留。filter 不會改變原數組,它返回過濾後的新數組。

假設我們有一個包含名稱和種類屬性的對象數組。我們想要創建一個只包含狗(species: "dog")的數組。如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = ;

  11. for (var i = 0; i < animals.length; i++) {

  12. if (animals[i].species === "dog") dogs.push(animals[i]);

  13. }

  14. console.log(dogs);

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = animals.filter(x => x.species === "dog");

  11. console.log(dogs); // {name: "Caro", species: "dog"}

  12. // { name: "Hamilton", species: "dog" }

reduce

reduce 方法對調用數組的每個元素執行回調函數,最後生成一個單一的值並返回。reduce 方法接受兩個參數:1)reducer 函數(回調),2)一個可選的 initialValue。

假設我們要對一個數組的求和:

  1. // 不使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. let sum = 0;

  4. for (let i = 0; i < arr.length; i++) {

  5. sum = sum + arr[i];

  6. }

  7. console.log(sum);//25

  1. // 使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);

  4. console.log(sum)//25

我們可以通過下圖,形象生動展示三者的區別:

"

前言

函數式編程在前端已經成為了一個非常熱門的話題。在最近幾年裡,我們看到非常多的應用程序代碼庫裡大量使用著函數式編程思想。

本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什麼是函數式的代碼、聲明式與命令式代碼的區別、以及常見的函數式模型都有哪些?更多優質文章請猛戳https://github.com/ljianshu/Blog

一、什麼是函數式編程

函數式編程是一種編程範式,主要是利用函數把運算過程封裝起來,通過組合各種函數來計算結果。函數式編程意味著你可以在更短的時間內編寫具有更少錯誤的代碼。舉個簡單的例子,假設我們要把字符串 functional programmingisgreat變成每個單詞首字母大寫,我們可以這樣實現:

  1. var string = 'functional programming is great';

  2. var result = string

  3. .split(' ')

  4. .map(v => v.slice(0, 1).toUpperCase + v.slice(1))

  5. .join(' ');

上面的例子先用 split 把字符串轉換數組,然後再通過 map 把各元素的首字母轉換成大寫,最後通過 join 把數組轉換成字符串。整個過程就是 join(map(split(str))),體現了函數式編程的核心思想:通過函數對數據進行轉換

由此我們可以得到,函數式編程有兩個基本特點:

  • 通過函數來對數據進行轉換

  • 通過串聯多個函數來求結果

二、對比聲明式與命令式

  • 命令式:我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。命令式代碼中頻繁使用語句,來完成某個行為。比如 for、if、switch、throw 等這些語句。

  • 聲明式:我們通過寫表達式的方式來聲明我們想幹什麼,而不是通過一步一步的指示。表達式通常是某些函數調用的複合、一些值和操作符,用來計算出結果值。

  1. //命令式

  2. var CEOs = ;

  3. for(var i = 0; i < companies.length; i++){

  4. CEOs.push(companies[i].CEO)

  5. }


  6. //聲明式

  7. var CEOs = companies.map(c => c.CEO);

從上面的例子中,我們可以看到聲明式的寫法是一個表達式,無需關心如何進行計數器迭代,返回的數組如何收集,它指明的是做什麼,而不是怎麼做。函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注於編寫業務代碼。

三、常見特性

無副作用

指調用函數時不會修改外部狀態,即一個函數調用 n 次後依然返回同樣的結果。

  1. var a = 1;

  2. // 含有副作用,它修改了外部變量 a

  3. // 多次調用結果不一樣

  4. function test1 {

  5. a++

  6. return a;

  7. }


  8. // 無副作用,沒有修改外部狀態

  9. // 多次調用結果一樣

  10. function test2(a) {

  11. return a + 1;

  12. }

透明引用

指一個函數只會用到傳遞給它的變量以及自己內部創建的變量,不會使用到其他變量。

  1. var a = 1;

  2. var b = 2;

  3. // 函數內部使用的變量並不屬於它的作用域

  4. function test1 {

  5. return a + b;

  6. }

  7. // 函數內部使用的變量是顯式傳遞進去的

  8. function test2(a, b) {

  9. return a + b;

  10. }

不可變變量

指的是一個變量一旦創建後,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程可以同時訪問同一個不可變變量,讓並行變得更容易實現。由於 JavaScript 原生不支持不可變變量,需要通過第三方庫來實現。(如 Immutable.js,Mori 等等)

  1. var obj = Immutable({ a: 1 });

  2. var obj2 = obj.set('a', 2);

  3. console.log(obj); // Immutable({ a: 1 })

  4. console.log(obj2); // Immutable({ a: 2 })

函數是一等公民

我們常說函數是JavaScript的"第一等公民",指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。下文將要介紹的閉包、高階函數、函數柯里化和函數組合都是圍繞這一特性的應用

四、常見的函數式編程模型

1.閉包(Closure)

如果一個函數引用了自由變量,那麼該函數就是一個閉包。何謂自由變量?自由變量是指不屬於該函數作用域的變量(所有全局變量都是自由變量,嚴格來說引用了全局變量的函數都是閉包,但這種閉包並沒有什麼用,通常情況下我們說的閉包是指函數內部的函數)。

閉包的形成條件:

  • 存在內、外兩層函數

  • 內層函數對外層函數的局部變量進行了引用

閉包的用途:可以定義一些作用域侷限的持久化變量,這些變量可以用來做緩存或者計算的中間量等

  1. // 簡單的緩存工具

  2. // 匿名函數創造了一個閉包

  3. const cache = (function {

  4. const store = {};


  5. return {

  6. get(key) {

  7. return store[key];

  8. },

  9. set(key, val) {

  10. store[key] = val;

  11. }

  12. }

  13. });

  14. console.log(cache) //{get: ƒ, set: ƒ}

  15. cache.set('a', 1);

  16. cache.get('a'); // 1

上面例子是一個簡單的緩存工具的實現,匿名函數創造了一個閉包,使得 store 對象 ,一直可以被引用,不會被回收。

閉包的弊端:持久化變量不會被正常釋放,持續佔用內存空間,很容易造成內存浪費,所以一般需要一些額外手動的清理機制。

2.高階函數

函數式編程傾向於複用一組通用的函數功能來處理數據,它通過使用高階函數來實現。高階函數指的是一個函數以函數為參數,或以函數為返回值,或者既以函數為參數又以函數為返回值

高階函數經常用於:

  • 抽象或隔離行為、作用,異步控制流程作為回調函數,promises,monads等

  • 創建可以泛用於各種數據類型的功能

  • 部分應用於函數參數(偏函數應用)或創建一個柯里化的函數,用於複用或函數複合。

  • 接受一個函數列表並返回一些由這個列表中的函數組成的複合函數。

JavaScript 語言是原生支持高階函數的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內置的一些高階函數,使用高階函數會讓我們的代碼更清晰簡潔。

map

map 方法創建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數後返回的結果。map 不會改變原數組。

假設我們有一個包含名稱和種類屬性的對象數組,我們想要這個數組中所有名稱屬性放在一個新數組中,如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = ;

  11. for (let i = 0; i < animals.length; i++) {

  12. names.push(animals[i].name);

  13. }

  14. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = animals.map(x=>x.name);

  11. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

filter

filter 方法會創建一個新數組,其中包含所有通過回調函數測試的元素。filter 為數組中的每個元素調用一次 callback 函數, callback 函數返回 true 表示該元素通過測試,保留該元素,false 則不保留。filter 不會改變原數組,它返回過濾後的新數組。

假設我們有一個包含名稱和種類屬性的對象數組。我們想要創建一個只包含狗(species: "dog")的數組。如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = ;

  11. for (var i = 0; i < animals.length; i++) {

  12. if (animals[i].species === "dog") dogs.push(animals[i]);

  13. }

  14. console.log(dogs);

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = animals.filter(x => x.species === "dog");

  11. console.log(dogs); // {name: "Caro", species: "dog"}

  12. // { name: "Hamilton", species: "dog" }

reduce

reduce 方法對調用數組的每個元素執行回調函數,最後生成一個單一的值並返回。reduce 方法接受兩個參數:1)reducer 函數(回調),2)一個可選的 initialValue。

假設我們要對一個數組的求和:

  1. // 不使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. let sum = 0;

  4. for (let i = 0; i < arr.length; i++) {

  5. sum = sum + arr[i];

  6. }

  7. console.log(sum);//25

  1. // 使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);

  4. console.log(sum)//25

我們可以通過下圖,形象生動展示三者的區別:

一文帶你瞭解JavaScript 函數式編程

3.函數柯里化

柯里化又稱部分求值,柯里化函數會接收一些參數,然後不會立即求值,而是繼續返回一個新函數,將傳入的參數通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數進行求值。

  1. // 普通函數

  2. function add(x,y){

  3. return x + y;

  4. }

  5. add(1,2); // 3

  6. // 函數柯里化

  7. var add = function(x) {

  8. return function(y) {

  9. return x + y;

  10. };

  11. };

  12. var increment = add(1);

  13. increment(2);// 3

這裡我們定義了一個 add 函數,它接受一個參數並返回一個新的函數。調用 add 之後,返回的函數就通過閉包的方式記住了 add 的第一個參數。那麼,我們如何來實現一個簡易的柯里化函數呢?

  1. function curryIt(fn) {

  2. // 參數fn函數的參數個數

  3. var n = fn.length;

  4. var args = ;

  5. return function(arg) {

  6. args.push(arg);

  7. if (args.length < n) {

  8. return arguments.callee; // 返回這個函數的引用

  9. } else {

  10. return fn.apply(this, args);

  11. }

  12. };

  13. }

  14. function add(a, b, c) {

  15. return [a, b, c];

  16. }

  17. var c = curryIt(add);

  18. var c1 = c(1);

  19. var c2 = c1(2);

  20. var c3 = c2(3);

  21. console.log(c3); //[1, 2, 3]

由此我們可以看出,柯里化是一種“預加載”函數的方法,通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的“緩存”,是一種非常高效的編寫函數的方法!

4.函數組合 (Composition)

前面提到過,函數式編程的一個特點是通過串聯函數來求值。然而,隨著串聯函數數量的增多,代碼的可讀性就會不斷下降。函數組合就是用來解決這個問題的方法。假設有一個 compose 函數,它可以接受多個函數作為參數,然後返回一個新的函數。當我們為這個新函數傳遞參數時,該參數就會「流」過其中的函數,最後返回結果。

  1. //兩個函數的組合

  2. var compose = function(f, g) {

  3. return function(x) {

  4. return f(g(x));

  5. };

  6. };


  7. //或者

  8. var compose = (f, g) => (x => f(g(x)));

  9. var add1 = x => x + 1;

  10. var mul5 = x => x * 5;

  11. compose(mul5, add1)(2);// =>15

歡迎關注公眾號前端工匠,你的成長我們一起見證!

"

前言

函數式編程在前端已經成為了一個非常熱門的話題。在最近幾年裡,我們看到非常多的應用程序代碼庫裡大量使用著函數式編程思想。

本文將略去那些晦澀難懂的概念介紹,重點展示在 JavaScript 中到底什麼是函數式的代碼、聲明式與命令式代碼的區別、以及常見的函數式模型都有哪些?更多優質文章請猛戳https://github.com/ljianshu/Blog

一、什麼是函數式編程

函數式編程是一種編程範式,主要是利用函數把運算過程封裝起來,通過組合各種函數來計算結果。函數式編程意味著你可以在更短的時間內編寫具有更少錯誤的代碼。舉個簡單的例子,假設我們要把字符串 functional programmingisgreat變成每個單詞首字母大寫,我們可以這樣實現:

  1. var string = 'functional programming is great';

  2. var result = string

  3. .split(' ')

  4. .map(v => v.slice(0, 1).toUpperCase + v.slice(1))

  5. .join(' ');

上面的例子先用 split 把字符串轉換數組,然後再通過 map 把各元素的首字母轉換成大寫,最後通過 join 把數組轉換成字符串。整個過程就是 join(map(split(str))),體現了函數式編程的核心思想:通過函數對數據進行轉換

由此我們可以得到,函數式編程有兩個基本特點:

  • 通過函數來對數據進行轉換

  • 通過串聯多個函數來求結果

二、對比聲明式與命令式

  • 命令式:我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。命令式代碼中頻繁使用語句,來完成某個行為。比如 for、if、switch、throw 等這些語句。

  • 聲明式:我們通過寫表達式的方式來聲明我們想幹什麼,而不是通過一步一步的指示。表達式通常是某些函數調用的複合、一些值和操作符,用來計算出結果值。

  1. //命令式

  2. var CEOs = ;

  3. for(var i = 0; i < companies.length; i++){

  4. CEOs.push(companies[i].CEO)

  5. }


  6. //聲明式

  7. var CEOs = companies.map(c => c.CEO);

從上面的例子中,我們可以看到聲明式的寫法是一個表達式,無需關心如何進行計數器迭代,返回的數組如何收集,它指明的是做什麼,而不是怎麼做。函數式編程的一個明顯的好處就是這種聲明式的代碼,對於無副作用的純函數,我們完全可以不考慮函數內部是如何實現的,專注於編寫業務代碼。

三、常見特性

無副作用

指調用函數時不會修改外部狀態,即一個函數調用 n 次後依然返回同樣的結果。

  1. var a = 1;

  2. // 含有副作用,它修改了外部變量 a

  3. // 多次調用結果不一樣

  4. function test1 {

  5. a++

  6. return a;

  7. }


  8. // 無副作用,沒有修改外部狀態

  9. // 多次調用結果一樣

  10. function test2(a) {

  11. return a + 1;

  12. }

透明引用

指一個函數只會用到傳遞給它的變量以及自己內部創建的變量,不會使用到其他變量。

  1. var a = 1;

  2. var b = 2;

  3. // 函數內部使用的變量並不屬於它的作用域

  4. function test1 {

  5. return a + b;

  6. }

  7. // 函數內部使用的變量是顯式傳遞進去的

  8. function test2(a, b) {

  9. return a + b;

  10. }

不可變變量

指的是一個變量一旦創建後,就不能再進行修改,任何修改都會生成一個新的變量。使用不可變變量最大的好處是線程安全。多個線程可以同時訪問同一個不可變變量,讓並行變得更容易實現。由於 JavaScript 原生不支持不可變變量,需要通過第三方庫來實現。(如 Immutable.js,Mori 等等)

  1. var obj = Immutable({ a: 1 });

  2. var obj2 = obj.set('a', 2);

  3. console.log(obj); // Immutable({ a: 1 })

  4. console.log(obj2); // Immutable({ a: 2 })

函數是一等公民

我們常說函數是JavaScript的"第一等公民",指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。下文將要介紹的閉包、高階函數、函數柯里化和函數組合都是圍繞這一特性的應用

四、常見的函數式編程模型

1.閉包(Closure)

如果一個函數引用了自由變量,那麼該函數就是一個閉包。何謂自由變量?自由變量是指不屬於該函數作用域的變量(所有全局變量都是自由變量,嚴格來說引用了全局變量的函數都是閉包,但這種閉包並沒有什麼用,通常情況下我們說的閉包是指函數內部的函數)。

閉包的形成條件:

  • 存在內、外兩層函數

  • 內層函數對外層函數的局部變量進行了引用

閉包的用途:可以定義一些作用域侷限的持久化變量,這些變量可以用來做緩存或者計算的中間量等

  1. // 簡單的緩存工具

  2. // 匿名函數創造了一個閉包

  3. const cache = (function {

  4. const store = {};


  5. return {

  6. get(key) {

  7. return store[key];

  8. },

  9. set(key, val) {

  10. store[key] = val;

  11. }

  12. }

  13. });

  14. console.log(cache) //{get: ƒ, set: ƒ}

  15. cache.set('a', 1);

  16. cache.get('a'); // 1

上面例子是一個簡單的緩存工具的實現,匿名函數創造了一個閉包,使得 store 對象 ,一直可以被引用,不會被回收。

閉包的弊端:持久化變量不會被正常釋放,持續佔用內存空間,很容易造成內存浪費,所以一般需要一些額外手動的清理機制。

2.高階函數

函數式編程傾向於複用一組通用的函數功能來處理數據,它通過使用高階函數來實現。高階函數指的是一個函數以函數為參數,或以函數為返回值,或者既以函數為參數又以函數為返回值

高階函數經常用於:

  • 抽象或隔離行為、作用,異步控制流程作為回調函數,promises,monads等

  • 創建可以泛用於各種數據類型的功能

  • 部分應用於函數參數(偏函數應用)或創建一個柯里化的函數,用於複用或函數複合。

  • 接受一個函數列表並返回一些由這個列表中的函數組成的複合函數。

JavaScript 語言是原生支持高階函數的, 例如Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce 是JavaScript中內置的一些高階函數,使用高階函數會讓我們的代碼更清晰簡潔。

map

map 方法創建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數後返回的結果。map 不會改變原數組。

假設我們有一個包含名稱和種類屬性的對象數組,我們想要這個數組中所有名稱屬性放在一個新數組中,如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = ;

  11. for (let i = 0; i < animals.length; i++) {

  12. names.push(animals[i].name);

  13. }

  14. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var names = animals.map(x=>x.name);

  11. console.log(names); //["Fluffykins", "Caro", "Hamilton", "Harold", "Ursula", "Jimmy"]

filter

filter 方法會創建一個新數組,其中包含所有通過回調函數測試的元素。filter 為數組中的每個元素調用一次 callback 函數, callback 函數返回 true 表示該元素通過測試,保留該元素,false 則不保留。filter 不會改變原數組,它返回過濾後的新數組。

假設我們有一個包含名稱和種類屬性的對象數組。我們想要創建一個只包含狗(species: "dog")的數組。如何實現呢?

  1. // 不使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = ;

  11. for (var i = 0; i < animals.length; i++) {

  12. if (animals[i].species === "dog") dogs.push(animals[i]);

  13. }

  14. console.log(dogs);

  1. // 使用高階函數

  2. var animals = [

  3. { name: "Fluffykins", species: "rabbit" },

  4. { name: "Caro", species: "dog" },

  5. { name: "Hamilton", species: "dog" },

  6. { name: "Harold", species: "fish" },

  7. { name: "Ursula", species: "cat" },

  8. { name: "Jimmy", species: "fish" }

  9. ];

  10. var dogs = animals.filter(x => x.species === "dog");

  11. console.log(dogs); // {name: "Caro", species: "dog"}

  12. // { name: "Hamilton", species: "dog" }

reduce

reduce 方法對調用數組的每個元素執行回調函數,最後生成一個單一的值並返回。reduce 方法接受兩個參數:1)reducer 函數(回調),2)一個可選的 initialValue。

假設我們要對一個數組的求和:

  1. // 不使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. let sum = 0;

  4. for (let i = 0; i < arr.length; i++) {

  5. sum = sum + arr[i];

  6. }

  7. console.log(sum);//25

  1. // 使用高階函數

  2. const arr = [5, 7, 1, 8, 4];

  3. const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);

  4. console.log(sum)//25

我們可以通過下圖,形象生動展示三者的區別:

一文帶你瞭解JavaScript 函數式編程

3.函數柯里化

柯里化又稱部分求值,柯里化函數會接收一些參數,然後不會立即求值,而是繼續返回一個新函數,將傳入的參數通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數進行求值。

  1. // 普通函數

  2. function add(x,y){

  3. return x + y;

  4. }

  5. add(1,2); // 3

  6. // 函數柯里化

  7. var add = function(x) {

  8. return function(y) {

  9. return x + y;

  10. };

  11. };

  12. var increment = add(1);

  13. increment(2);// 3

這裡我們定義了一個 add 函數,它接受一個參數並返回一個新的函數。調用 add 之後,返回的函數就通過閉包的方式記住了 add 的第一個參數。那麼,我們如何來實現一個簡易的柯里化函數呢?

  1. function curryIt(fn) {

  2. // 參數fn函數的參數個數

  3. var n = fn.length;

  4. var args = ;

  5. return function(arg) {

  6. args.push(arg);

  7. if (args.length < n) {

  8. return arguments.callee; // 返回這個函數的引用

  9. } else {

  10. return fn.apply(this, args);

  11. }

  12. };

  13. }

  14. function add(a, b, c) {

  15. return [a, b, c];

  16. }

  17. var c = curryIt(add);

  18. var c1 = c(1);

  19. var c2 = c1(2);

  20. var c3 = c2(3);

  21. console.log(c3); //[1, 2, 3]

由此我們可以看出,柯里化是一種“預加載”函數的方法,通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的“緩存”,是一種非常高效的編寫函數的方法!

4.函數組合 (Composition)

前面提到過,函數式編程的一個特點是通過串聯函數來求值。然而,隨著串聯函數數量的增多,代碼的可讀性就會不斷下降。函數組合就是用來解決這個問題的方法。假設有一個 compose 函數,它可以接受多個函數作為參數,然後返回一個新的函數。當我們為這個新函數傳遞參數時,該參數就會「流」過其中的函數,最後返回結果。

  1. //兩個函數的組合

  2. var compose = function(f, g) {

  3. return function(x) {

  4. return f(g(x));

  5. };

  6. };


  7. //或者

  8. var compose = (f, g) => (x => f(g(x)));

  9. var add1 = x => x + 1;

  10. var mul5 = x => x * 5;

  11. compose(mul5, add1)(2);// =>15

歡迎關注公眾號前端工匠,你的成長我們一起見證!

一文帶你瞭解JavaScript 函數式編程

參考文章

  • 珠峰架構課(推薦)

  • MDN文檔

  • What is Functional Programming?

  • So You Want to be a Functional Programmer

  • 理解 JavaScript 中的高階函數

  • 我所瞭解的函數式編程

  • JS函數式編程指南

  • JavaScript函數式編程(一)

  • 我眼中的 JavaScript 函數式編程

"

相關推薦

推薦中...