iT邦幫忙

2022 iThome 鐵人賽

DAY 25
1
Modern Web

致 JavaScript 開發者的 Functional Programming 新手指南系列 第 25

Day 25:高階函數與複合函數(3):reduce 實用方法

  • 分享至 

  • xImage
  •  

在 FP 中,會發現我們其實沒有那麼常使用 forforEach ,更多時候是使用 filtermap 陣列方法,因為在 FP 這個設計模式中,為了讓程式碼的產出可以符合預期,我們會竟量避免使用可能會有副作用或是讓資料污染的狀況。

除了 mapfilter 外,在 FP 中我們也很常使用 reduce 來進行資料的處理,在聊聊 reduce 在 FP 的應用前,讓我們先來簡單了解 reduce 的使用方法吧!

reduce 基礎應用

reduce() 是一個可以遍歷陣列的方法,通常作為累加器使用。

我們會在 reduce() 方法中帶入一個 callback 函式(這個函式稱為 reducer),陣列中的元素會在遍歷的過程,一一執行這個 reducer 函式,reducer 函式的第一個參數會是作為 reduce() 函式第二個參數的初始值,而 reducer 的第二個參數會是遍歷當前的元素:

const array1 = [1, 2, 3, 4];

// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
  (previousValue, currentValue) => previousValue + currentValue,
  initialValue
);

上方範例的 initialValue 會在 reduce() 遍歷第一筆元素時,作為初始值,也就是 reducer 函式第一個參數 previousValuereducer 函式會回傳一個計算完的值(在這邊的範例為 previousValue + currentValue ),在下一次的遍歷中,作為 previousValue 傳入reducer 函式。

由於 reduce() 陣列方法可以在第二個參數傳入初始值的關係,所以可以進行比 map()filter() 更複雜的計算,雖然我們可以透過 reduce 進行更複雜的計算,但在 MDN 文件中,曾提及使用 reduce() 的壞處:

reduce() 這種類型的遞迴函式非常強大,但對 JavaScript 開發經驗比較少的人來說,有時候會比較費解一點。如果要讓程式碼更簡潔時,可能就要衡量程式碼簡潔度與可讀性哪個比較重要。」

但當然,我們可以透過語意化的命名,甚至是搭配抽象化的方式讓 reduce() 更好被理解。

基於易讀性的關係,也許接下來的範例不像我們平常所撰寫的程式碼所直覺,但只要習慣後,會有:「啊!原來如此!」的感覺,接著就會對程式碼要如何進行抽象化越來越有感覺。

接著就讓我們來看看 reduce 除了可以進行數字的累加外,還能解決什麼問題吧!

使用 reduce 攤平、重組資料結構

在 FP 設計模式中,我們時常會將資料作為最後的考量(Data Last),也就是說資料結構不應該影響函式的運作,此時我們就需要非常豐富資料處理的手段,例如我們可能會需要將多層級的陣列或是物件進行,層級的拆解,這樣的過程被稱為攤平( Flatten)。

舉例來說,我們可以透過 reduce() 來攤平陣列:

const flattened = [[0, 1], [2, 3], [4, 5]].reduce(
  (previousValue, currentValue) => previousValue.concat(currentValue),
  [],
);
// [0, 1, 2, 3, 4, 5]

我們在 reduce() 中傳入空陣列作為初始值,並透過 concat() 方法進行陣列的串接,透過這樣的運算就可以將陣列的資料結構給攤平了,透過攤平的手法,我們可以將原本無法被套用陣列方法的資料,轉化爲可被遍歷的資料結構。
當然,我們可以利用相同手法來重組物件的資料結構:

// 物件屬性篩選器
const filterProps = obj => list => {
  const keys = Object.keys(obj);
  const filterProps = list;
  return keys.reduce((prev, curr) => {
    if (filterProps.find(i => i === curr)) {
      return { ...prev, [curr]: obj[curr] };
    }
    return prev;
  }, {})
};

// test function
const list = ['a', 'b'];
const obj = { a: 1, b: 1, d: 1 };
const newObj = filterProps(obj)(list);
// ->  { a: 1, b: 1};

我們可以透過在 reduce() 傳入空物件的方式,進行我們想要資料重組的運算,舉例來說,我們只想要保留物件屬性中的特定幾個屬性,我們只要負責將想要重組的物件、想要保留的屬性列表傳入 filterProps 函式,就可以獲得我們想要重組的物件。

在上方的範例中,我們甚至將 filterProps 進行了科里化,這樣我就可以透過抽象化中特殊化的手法,重複利用指定的局部性應用函式。

當然,這些範例都可以再被優化或是更簡潔,就如同上方所提及,在使用 reduce() 時可讀性是非常重要的。

使用 reduce 建立 pipe

上面的範例中,我們大致上知道我們可以透過使用 reduce() 把資料結構進行排列組合達到不一樣的效果,但其實我們還可以透過 reduce() 搭配多組函式,替我們的函式進行自動化,也就是所謂管線(Pipe)的概念。

舉例來說,我們在實務開發中可能會遇到需要進行連續性的資料處理,拿到 A 資料後,進行 B 處理,再拿 B 處理完的資料進行 C 處理,此時我們就可以將要處理的資料作為初始值傳入 ,再透過逐步執行指定的函式後回傳結果,回傳的結果將做完下一次函式的初始值:

// 可以自動化同步函式的 pipe 函式
const pipe = init => funcs => funcs.reduce(((x, func) => func(x) ), init);

如果我們不透過 reduce() 來進行連續性的處理的話,我們的程式碼可能就會像是:

const a = 1;
const b = fn1(a);
const c = fn2(b);

但其實我們完全可以把上述的流程所會用到的函式,整理成有順序的陣列,再把這個整理好的陣列,與一開始的初始資料交給 pipe() 函式處理。

這也是為什麼每當人們提到 FP ,就會聯想到 reduce() 的原因,因為它能做到的事真的太多了!

講到這邊,可能會覺得:「如果我不熟 FP 該怎麼辦呢?」

畢竟對於開發經驗不多的人來說,要將這些看似簡單,但實際上有很多學問的函式進行重複組合,其實並不好上手,所以在下一個章節,我們要來介紹一些第三方函式庫,來幫助我們在寫程式的過程中不僅有範本可以參考,更能透過前人之手幫我們來解決更複雜的計算任務。

那我們就下一個章節見吧!

參考資料:

  1. MDN - reduce

上一篇
Day 24:高階函數與複合函數(2):科里化陣列方法
下一篇
Day 26 :第三方函式庫(1):初識Lodash.js
系列文
致 JavaScript 開發者的 Functional Programming 新手指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言