簡單趣談 Functional Programming in JavaScript

前言

Functional Programming 可以說是一個非常好玩的設計模式,為什麼需要 Functional Programming 的設計模式呢?簡單來講就是要避免一些問題 (Side Effect),所以就乾脆寫一篇很入門的、很簡單的 Functional Programming in JavaScript。

Side Effect (副作用)

開始介紹 Functional Programming 之前,我想先聊一些東西,第一個也就是 Side Effect。

什麼是 Side Effect 呢?Side Effect 中文稱之為「副作用」,副作用這一詞,非常常見在一個地方,也就是你看醫生後所拿到的感冒藥會有哪些副作用,例如:頭痛、嗜睡以及身體痠痛等這些副作用。

但是 Side Effect 本身也可以說是有幫助的、有益處的,只是大多都會用來形容不良的反應居多,例如你吃了感冒藥用的副作用有感冒好轉以及嗜睡等。

副作用不代表壞的,也有好的意思。

所以你可以把 Side Effect 理解成「當你執行某個函式或是行為時,會導致原有的狀態被修改或附加功能」,這些就稱之為 Side Effect,例如:全域變數被修改或者是輸出一個系統訊息等等,這些都算是一個 Side Effect。

這時候你應該會覺得很奇妙,為什麼輸出訊息 (console.log()) 也會算是一個 Side Effect 呢?我們來看一段範例程式碼,或許你會更清楚一點

1
2
3
4
5
6
7
8
function sayHello(name) {
console.log('Say');
return 'Hello' + name;;
}

sayHello('Ray');
// Say
// Hello Ray

我們可以看到 sayHello 這個函式會將結果輸出到 console tab 兩次,那…為什麼這樣算是一個 Side Effect 呢?

我們剛剛呼叫了 sayHello 這個函式,但是它的運算 or 執行結果是不能影響到外層的,而 sayHello 這個函式在執行後卻發生會輸出結果到「本身函式之外」,而這也就是為什麼 console.log 本身就是一個 Side Effect。

請注意一件事情,就算這個運算行為或者是執行動作不是包在函式內,只要會更改原始值都算是一個 Side Effect

1
2
3
var myName = 'Ray';

myName = 'Array';

意想不到吧?連這種我們常認為理所當然的重新賦予值的行為都算是 Side Effect 的一種,因此 Side Effect 在 JavaScript 中非常常見

  • AJAX
  • DOM 操作
  • 時間相關操作(setTimeout)
  • 修改外部參數
    ..等等

怕了吧,我們常見的開發行為都有 Side Effect 的存在,那麼這不代表叫你只要有 Side Effect 的東西就通通不要使用,而是應該要去理解並且釐清它的運作,這樣子才可以避免一些無法預期的問題,否則你難道要連 console.log 都不用嗎?(笑

那麼早期我有寫一篇「什麼是 ESM(ES6 Modules or JavaScript Modules) 呢?」裡面就有提到一個 Side Effect 模組的部分,因此適當使用 Side Effect 以及了解 Side Effect 是可以幫助開發的。

Pure & Non-Pure Function (純與不純的函式)

Pure Function 中文又稱之為純粹函式、純函式,什麼是 Pure Function 呢?

開始之前,人家都說好的不學,壞的學特別快 (所以就讓我們先來學壞的)

Non-Pure Function (不純函式)

Non-Pure Function,也就是不純的函式,那什麼是不純的函式呢?讓我們看一段簡單的 Non-Pure Function Example Code:

1
2
3
4
5
6
7
var myName =  'Ray';

function sayHello() {
console.log('Hello' + myName);
}

sayHello();

上面程式碼看起來很正常、很理所當然,但實際上 sayHello 這個函式是會直接取用 Global 的變數,而這種狀況我們可能會因為一些操作而導致污染 myName 這個變數,因此它是一個 Non-Pure Function,Non-Pure Function 也可能會導致一些 Side Effect 的狀況。

Pure Function (純函式)

那什麼是 Pure Function 呢?如果拿前面的範例來調整的話,就會變成以下:

1
2
3
4
5
function sayHello(name) {
return 'Hello' + name;
}

const myName = sayHello('Ray');

Pure Function 會保持著一個原則,也就是一個輸入 (input) 與輸出 (output),那這就是一個 Pure Function。

因此 Pure Function 遵守一個規則也是…

one input, one output

Higher-Order Function(高階函式)

Higher-Order Function 又稱之為 HOF,簡單來講就是函式可以被傳入到其他函式或者是回傳一個函式,只要滿足這兩個條件,那麼它就是一個 Higher-Order Function。

我知道這邊還是很抽象,所以就來看一些範例程式碼,首先是「函式可以被傳入函式」

1
2
3
4
5
6
7
8
9
10
function fn(callback) {
console.log('fn');
}

function HOF(callback) {
console.log('HOF');
callback();
}

HOF(fn);

又或者是…

1
2
3
4
5
6
7
8
9
10
const fn = (callback) => {
console.log('fn');
}

const HOF = (callback) => {
console.log('HOF');
callback();
}

HOF(fn);

上面都是屬於函式可以被傳入到函式,而另一種回傳一個函式呢?相信聰明的你應該已經知道有什麼東西是這種狀況了,也就是閉包(Closure)

1
2
3
4
5
6
7
8
function closure() {
console.log('closure');
return function (myName) {
console.log('myName is', myName);
}
}

closure()('Ray');

而基本上在 JavaScript 中很多東西都屬於一個 HOF,如果你想知道更多的話,歡迎參考「關於 JavaScript 陣列 20 種操作的方法」。

Don’t Use Iterate (不要使用迭代)

接下來聊一下關於 iterate,也就是迭代。

什麼是迭代呢?也就是我們所謂的迴圈,例如:forwhile 這類型的迴圈

1
2
3
4
5
var list = [1, 2, 3];

for(var i = 0; i < list.length; i+=1){
...
}

為什麼這樣說呢?主要原因在於往往我們在使用陣列語法時,通常會針對陣列有一些操作,例如數字全部增加 1

1
2
3
4
5
6
7
var list = [1, 2, 3];

for(var i = 0; i < list.length; i+=1){
list[i] +=1;
}

console.log(list); // [ 2, 3, 4 ]

而這樣子就會導致 Side Effect (副作用)發生,因此我們要去避免這種狀況,例如改使用 map() 語法

1
2
3
4
5
6
var list = [1, 2, 3];

var newList = list.map((i) => i +=1);

console.log(newList); // [ 2, 3, 4 ]
console.log(list); // [ 1, 2, 3 ]

透過一些 ES6 語法我們可以避免修改到原始的陣列,因此掌握 ES6 的高階函式語法是非常重要的。

Avoid Mutability (避免變異)

Avoid Mutability 簡單來講就是要避免修改不可變的變數,舉例來講 Side Effect 有舉例一個範例程式碼

1
2
3
var myName = 'Ray';

myName = 'Array';

如果預期這個名字是不能更改的話,那麼就不應該這樣寫,但這時候或許你會想說..「那我改用 const 宣告就好了呀?」

1
2
3
const myName = 'Ray';

myName = 'Array'; // 噴錯

沒錯,確實單純的變數宣告上是可以透過宣告方式 (constlet) 來避免,但如果今天是一個陣列、物件呢?

1
2
3
4
5
const arr = [1, 2, 3];

arr[1] = 4;

console.log(arr); // [ 1, 4, 3 ]

實際開發上這種 Mutability 是非常危險的,畢竟你可能認為你原本的陣列還是 [1, 2, 3],但實際上根本就已經不知道在哪一處被改成了 [ 1, 4, 3 ],最終這樣子在開發時就很難 Debug。

因此最好任何宣告的變數都是不變的,這樣子才可以盡可能避免這些狀況發生,那如果以上面那個範例來講,我們可以使用一些方法來避免

1
2
3
4
5
6

const newArr = JSON.parse(JSON.stringify(arr));
newArr[1] = 4

console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 4, 3 ]

當然你也可以使用 map 來重新產生一個陣列

1
2
3
4
const arr = [1, 2, 3];
const newArr = arr.map((item) => item === 2 ? 4 : item);
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 4, 3 ]

結尾

這一篇其實主要是觀看了 Learning Functional Programming with JavaScript - Anjana Vakil - JSUnconf 所寫下來的心得,其實這一部影片我認為對於想初入學習 Functional Programming 的人,是真的滿好理解且上手,當然 Functional Programming 不單只有這樣子,只是這一部影片用很簡單的時間與輕鬆愉快的方式讓我們初步了解到 Functional Programming 的好玩之處。

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你也可以考慮請我喝一杯咖啡

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ