Skip to content

函數式程式設計

Functional Programming (FP) 是一種編程範式(paradigm),它強調使用純函數(pure function)和不可變數據結構(immutable data structure)來寫程式。FP 的優點有:

  • 減少錯誤、bug,因為純函數不會產生副作用,也不會修改外部狀態。
  • 提高可讀性和可維護性,因為純函數的輸出只取決於輸入,不受外部因素影響,所以更容易理解和測試。
  • 提高可控數據流,因為純函數可以任意組合成新的函數,並且保持其純度和不可變性,所以可以更靈活地處理數據流。

JavaScript 是一種多範式的語言,它支持 FP 的特性,例如:

  • 使用箭頭函數 (arrow function) 簡化函數的定義和語法。
  • 將函數作為一級公民,可以將函數作為參數傳遞或作為返回值返回。
函數作為一級公民

js 指函數可以像其他類型的值一樣被賦值、傳遞或返回:

js
// 1. 將函數賦值給變量
const add = (a, b) => a + b;

// 2. 將函數作為參數傳遞給另一個函數
const multiply = (x, y) => x * y;
const apply = function(f, x, y) => f(x, y);
console.log(apply(multiply, 3, 4)); // 12

// 3. 將函數作為另一個函數的返回值
const makeCounter = () => {
  let count = 0;
  return () => {
    count++;
    return count;
  };
};

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 1. 將函數賦值給變量
const add = (a, b) => a + b;

// 2. 將函數作為參數傳遞給另一個函數
const multiply = (x, y) => x * y;
const apply = function(f, x, y) => f(x, y);
console.log(apply(multiply, 3, 4)); // 12

// 3. 將函數作為另一個函數的返回值
const makeCounter = () => {
  let count = 0;
  return () => {
    count++;
    return count;
  };
};

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
  • 使用高階函數 (higher-order function) 來抽象和封裝邏輯,例如 map, filter, reduce 等。
高階函數

高階函數(higher-order function)是一種可以接收或返回另一個函數的函數,它可以實現抽象化、模組化和函數式編程的效果。 Array.prototype.map()就是一個高階函數,它接受一個函數作為參數,並對陣列中的每個元素執行該函數,返回一個新的陣列。 下面是一個用js寫的高階函數的例子:

js
const double = (num) => num * 2;
let arr = [1, 2, 3, 4, 5];
let newArr = arr.map(double);
console.log(newArr); // [2, 4, 6, 8, 10]
const double = (num) => num * 2;
let arr = [1, 2, 3, 4, 5];
let newArr = arr.map(double);
console.log(newArr); // [2, 4, 6, 8, 10]
  • 使用閉包 (closure) 來實現私有變量和模擬模組。
閉包
  • 閉包 (closure) 是一種函數,可以記住它被創建時的環境,也就是說,它可以訪問它所在的外部函數的變量和參數。閉包可以實現私有變量和模擬模組。
  • 私有變量是指只能在一個函數內部訪問,而不能被外部訪問的變量。
  • 模擬模組指用 js 實現類似於其他語言中的模組或包的功能,也就是說,可以將一些相關的代碼組織在一起,形成一個獨立的單元,並且可以控制哪些代碼是對外暴露的,哪些代碼是隱藏的。

例如,我們可以定義一個立即執行函數表達式 (IIFE),它會返回一個物件,這個對象包含了我們想要暴露給外部的代碼,而函數內部則可以定義一些私有的變量或函數,這些只能被內部使用,而不能被外部訪問。這樣,我們就創建了一個模擬的模組以及私有變量。例如:

js
// 定義一個立即執行函數表達式,它返回一個對象,這個對象就是我們的模組
let mathModule = (function() {
  // 這裡的 PI 就是一個私有變量,只能被內部的函數訪問
  let PI = 3.14;
  // 返回一個對象,包含了我們想要暴露的函數
  return {
    // 這個函數可以計算圓的面積,它可以訪問 PI 變量
    areaOfCircle: function(r) {
      return PI * r * r;
    },
    // 這個函數可以計算圓的周長,它也可以訪問 PI 變量
    circumferenceOfCircle: function(r) {
      return 2 * PI * r;
    }
  };
})();

// 使用模組中的函數
console.log(mathModule.areaOfCircle(5)); // 78.5
console.log(mathModule.circumferenceOfCircle(5)); // 31.4
// 嘗試直接訪問 PI 變量
console.log(mathModule.PI); // undefined
// 定義一個立即執行函數表達式,它返回一個對象,這個對象就是我們的模組
let mathModule = (function() {
  // 這裡的 PI 就是一個私有變量,只能被內部的函數訪問
  let PI = 3.14;
  // 返回一個對象,包含了我們想要暴露的函數
  return {
    // 這個函數可以計算圓的面積,它可以訪問 PI 變量
    areaOfCircle: function(r) {
      return PI * r * r;
    },
    // 這個函數可以計算圓的周長,它也可以訪問 PI 變量
    circumferenceOfCircle: function(r) {
      return 2 * PI * r;
    }
  };
})();

// 使用模組中的函數
console.log(mathModule.areaOfCircle(5)); // 78.5
console.log(mathModule.circumferenceOfCircle(5)); // 31.4
// 嘗試直接訪問 PI 變量
console.log(mathModule.PI); // undefined

從上面的例子可以看出,使用閉包的好處: 2. 減少全域變量的使用,避免命名衝突和污染全局作用域。

  1. 保護數據的安全性和完整性,避免被外部修改或污染。
  2. 提高代碼的可讀性和可維護性(將相關的數據和行為封裝在一起)。
  3. 實現代碼的模組化、結構化、抽象和隱藏,提高代碼的重用性和可測試性。
  • 使用柯里化 (currying) 和偏函數 (partial application) 來創建特定用途的函數。
柯里化

柯里化(currying)是一種將多參數函數轉換為單參數函數的技術,它可以實現函數的部分應用(partial application),也就是預先固定一些參數,返回一個新的函數,等待剩餘的參數傳入。柯里化的好處是可以創造一些通用的函數,減少代碼重複,提高可讀性和可維護性

js
// 定義一個兩個參數的函數
function add(x, y) {
  return x + y + z;
}

// 定義一個柯里化的函數
function curry(fn) {
  // 獲取原始函數的參數個數
  let arity = fn.length;
  // 創建一個接收參數的數組
  let args = [];
  // 返回一個新的函數
  return function curried() {
    // 將當前傳入的參數合併到數組中
    args = args.concat(Array.from(arguments));
    // 如果參數個數小於原始函數的參數個數,則繼續返回新的函數
    if (args.length < arity) {
      return curried;
    } else {
      // 否則,調用原始函數並返回結果
      return fn.apply(null, args);
    }
  };
}

// 使用柯里化的函數對add進行轉換
let curriedAdd = curry(add);

// 測試不同的調用方式
console.log(curriedAdd(1, 2, 3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
// 定義一個兩個參數的函數
function add(x, y) {
  return x + y + z;
}

// 定義一個柯里化的函數
function curry(fn) {
  // 獲取原始函數的參數個數
  let arity = fn.length;
  // 創建一個接收參數的數組
  let args = [];
  // 返回一個新的函數
  return function curried() {
    // 將當前傳入的參數合併到數組中
    args = args.concat(Array.from(arguments));
    // 如果參數個數小於原始函數的參數個數,則繼續返回新的函數
    if (args.length < arity) {
      return curried;
    } else {
      // 否則,調用原始函數並返回結果
      return fn.apply(null, args);
    }
  };
}

// 使用柯里化的函數對add進行轉換
let curriedAdd = curry(add);

// 測試不同的調用方式
console.log(curriedAdd(1, 2, 3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
  • 使用純函式實現不可變數據結構、避免副作用。
不可變數據結構

意味著一旦建立了一個資料結構,例如陣列或物件,就不能對它進行修改,每次操作都會回傳一個新創建的資料結構,原本的資料保持不變。

js
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let newArr = arr.map(x => x + 1); // 使用 map 函式回傳一個新的陣列
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(newArr); // [2, 3, 4, 5, 6] 新的陣列儲存了修改後的資料
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let newArr = arr.map(x => x + 1); // 使用 map 函式回傳一個新的陣列
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(newArr); // [2, 3, 4, 5, 6] 新的陣列儲存了修改後的資料

同理,如果我們想要過濾掉一個陣列中小於三的元素,我們可以使用 filter 函式,如下:

js
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let newArr = arr.filter(x => x >= 3); // 使用 filter 函式回傳一個新的陣列
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(newArr); // [3, 4, 5] 新的陣列儲存了過濾後的資料
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let newArr = arr.filter(x => x >= 3); // 使用 filter 函式回傳一個新的陣列
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(newArr); // [3, 4, 5] 新的陣列儲存了過濾後的資料

最後,如果我們想要對一個陣列中的所有元素進行某種計算,並得到一個單一的結果,我們可以使用 reduce 函式,如下:

js
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let sum = arr.reduce((acc, x) => acc + x); // 使用 reduce 函式回傳一個數值
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(sum); // 15 新的數值儲存了陣列中所有元素的和
let arr = [1, 2, 3, 4, 5]; // 原本的陣列
let sum = arr.reduce((acc, x) => acc + x); // 使用 reduce 函式回傳一個數值
console.log(arr); // [1, 2, 3, 4, 5] 原本的陣列沒有改變
console.log(sum); // 15 新的數值儲存了陣列中所有元素的和