跳至主要内容

課程:JavaScript 與 React 底層原理 第 9 堂:ES6+ 語法收尾

29 函數式陣列操作

想像你正在開發一個電商網站的後台。API 回傳了一個包含 500 個訂單的陣列,你需要完成以下任務:首先,篩選出今天成交的訂單;接著,從中提取每個訂單的總金額;最後,計算出今天的總營收。

如果你使用傳統的 for 迴圈,你可能需要建立好幾個暫存變數,寫下複雜的索引邏輯,還要小心翼翼地處理迴圈邊界。但如果我告訴你,這一切可以用一種像「生產線流水線」一樣優雅、且幾乎不會出錯的方式完成呢?

在 JavaScript 的世界裡,這就是 函數式陣列操作 的魅力所在。這不僅是現代 JS 開發的核心,更是 React 渲染列表、處理狀態(State)時唯一推薦的標準做法。

從命令式到宣告式:思維的轉向

在深入具體的語法之前,我們必須先理解一種程式設計哲學的轉變:從 命令式(Imperative) 轉向 宣告式(Declarative)

命令式:告訴電腦「怎麼做」

傳統的 for 迴圈是命令式的典型代表。你必須詳細交代每一個步驟:

  1. 建立一個空的結果陣列。
  2. 宣告一個索引 i 從 0 開始。
  3. 判斷 i 是否小於陣列長度。
  4. 取得目前元素並判斷條件。
  5. 將符合條件的元素推入結果陣列。
  6. i 加 1 並重複。

這種寫法的問題在於,程式碼充斥著控制邏輯(如何遍歷、如何管理索引),而真正的業務邏輯(篩選與轉換)卻被淹沒在其中。

宣告式:告訴電腦「我要什麼」

函數式陣列操作(如 mapfilter)則是宣告式的。你不再關心迴圈怎麼跑、索引怎麼算,你只需要傳達你的意圖:

  • 「我要篩選出金額大於 100 的項目。」
  • 「我要將每個項目轉換成 HTML 標籤。」

這就是為什麼在 React 中你幾乎看不到 for 迴圈,因為 React 本身就是宣告式 UI 框架。我們描述 UI 「應該長什麼樣子」,而不是命令瀏覽器「如何一步步修改 DOM」。


不可變性(Immutability):React 的生命線

在使用 mapfilterreduce 之前,你必須牢記一個最高準則:這些方法永遠不會修改原始陣列。

它們會執行邏輯,然後回傳一個全新的陣列(或是 reduce 產生的新值)。這在 JS 中被稱為「無副作用(Side-effect free)」的操作。

為什麼這對 React 如此重要? React 的渲染機制高度依賴「參考比較(Reference Equality)」。當你呼叫 setItems(newItems) 時,React 會快速檢查 newItems 的記憶體位址是否與舊的 items 不同。如果不同,它就知道資料變了,該重新渲染。

如果你使用 pushsplice 直接修改原始陣列,記憶體位址沒變,React 可能會認為資料沒有更新,進而導致畫面不觸發重繪的靈異現象。因此,掌握這些回傳新陣列的方法,是寫好 React 的基本功。


map:資料的 1 對 1 轉換

map 是 React 開發者最親密的朋友。它的核心功能是:將陣列中的每一個元素,按照你提供的函數邏輯,「映射」成另一個新的元素。

運作邏輯

  • 輸入:長度為 N 的陣列。
  • 輸出:長度同樣為 N 的新陣列。
  • 特性:它是一對一的,輸入幾個,產出就是幾個。

範例:從資料到 UI 的橋樑

假設我們有一組使用者資料:

const users = [
{ id: 1, name: 'Aria', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
];

// 我們想將這組物件,轉換成 React 渲染需要的標籤(這裡用字串模擬)
const userElements = users.map((user) => {
return `<li>${user.name}</li>`;
});

console.log(userElements);
// 產出:["<li>Aria</li>", "<li>Bob</li>", "<li>Charlie</li>"]

在 React 中的實戰預覽

在 JSX 中,我們直接在大括號內呼叫 map 來生成列表:

<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>

注意: 這裡我們看到了 key 屬性。這是 React 的一個優化機制。雖然我們在 Topic 7 才會深入探討 Reconciliation,但現在請記住:當你用 map 產出元件列表時,必須給每個元素一個唯一的 key,這樣 React 才能在資料變動時,精確地知道哪一個 DOM 節點需要更新,而不是全部砍掉重練。


filter:資料的守門員

filter 的工作非常單純:根據你提供的條件,決定哪些元素可以留下來,哪些該被剔除。

運作邏輯

  • 輸入:長度為 N 的陣列。
  • 回傳:一個布林值(true 代表保留,false 代表捨棄)。
  • 輸出:一個長度小於或等於 N 的新陣列。

範例:篩選活躍使用者

延續剛才的 users 例子,如果我們只想顯示正在線上的使用者:

const activeUsers = users.filter((user) => {
// 只有當 user.active 為 true 時,這個元素才會出現在新陣列中
return user.active;
});

console.log(activeUsers);
// 產出:兩個物件的陣列(Aria 和 Charlie)

思考題:如果沒有人符合條件會怎樣?

如果你執行 filter 但沒有任何元素符合條件,它會回傳一個空的陣列 [],而不是 nullundefined。這在 React 中非常安全,因為 [].map(...) 不會報錯,只會單純地不渲染任何東西,避免了程式當掉的風險。


reduce:陣列操作的萬用瑞士刀

如果說 map 是轉換、filter 是挑選,那麼 reduce 就是「折疊」。它能將整個陣列濃縮成一個單一的值。這個值可以是數字、字串、一個物件,甚至可以是另一個完全不同結構的陣列。

reduce 是初學者最容易卡關的地方,但只要掌握了它的「累加器」概念,你就能處理最複雜的資料結構。

核心術語

reduce 接受兩個參數:

  1. Callback 函數:定義如何處理每個元素。它接收兩個重要參數:
  • accumulator (acc): 累加器,它是上一次函數執行的結果。
  • currentValue (cur): 目前正在處理的陣列元素。
  1. 初始值 (Initial Value):這是 accumulator 的起點。強烈建議永遠提供初始值。

範例:計算總積分

const scores = [10, 20, 30, 40];

// 我們想計算總分
const totalScore = scores.reduce((acc, cur) => {
console.log(`目前累加:${acc}, 目前數值:${cur}`);
return acc + cur;
}, 0); // 這裡的 0 就是初始值

console.log(totalScore); // 100

進階應用:將陣列轉換為物件

在前端開發中,我們常需要將陣列轉成「以 ID 為 Key」的物件,以便快速查找。這正是 reduce 的拿手好戲:

const userMap = users.reduce((acc, user) => {
// 將 user.id 作為 key,user 物件本身作為 value
acc[user.id] = user;
return acc;
}, {}); // 初始值是一個空物件

console.log(userMap[1]); // { id: 1, name: 'Aria', ... }

為什麼 **reduce** 被稱為瑞士刀? 因為理論上,你可以用 reduce 實作出 mapfilter 的所有功能。它是陣列方法中最底層、最強大的工具。當你發現 mapfilter 組合起來仍難以處理某些複雜邏輯時(例如:需要在一次遍歷中同時完成篩選與計算),reduce 就是你的終極解法。


鏈式呼叫(Chaining):建立資料處理管線

這就是函數式編程最美妙的時刻。因為 mapfilter 都回傳陣列,我們可以像接水管一樣,將多個操作串連在一起。

實戰場景:從原始資料到最終統計

假設我們拿到一份原始訂單資料,我們要執行以下動作:

  1. 只看「已完成」的訂單(filter)。
  2. 提取這些訂單的「金額」(map)。
  3. 計算「總營收」(reduce)。
const orders = [
{ id: 101, status: 'completed', amount: 500 },
{ id: 102, status: 'pending', amount: 1000 },
{ id: 103, status: 'completed', amount: 250 },
{ id: 104, status: 'cancelled', amount: 300 }
];

const totalRevenue = orders
.filter(order => order.status === 'completed') // 先過濾
.map(order => order.amount) // 再轉型
.reduce((total, price) => total + price, 0); // 最後總計

console.log(totalRevenue); // 750

這段程式碼讀起來幾乎就像英文句子一樣自然:「從訂單中,過濾出完成的,拿出金額,然後加總。」比起寫三個 for 迴圈並手動維護中間變數,這種寫法極大地降低了認知負擔,也更容易除錯。


總結:React 中的陣列操作思維

在 React 專案中,你會不斷重複以下模式:

  1. 獲取資料:從 API 拿回一個陣列。
  2. 準備資料:使用 filter 排除不需要的、使用 map 格式化日期或金額。
  3. 渲染資料:在 JSX 中使用 map 將處理後的資料轉換為元件,並記得補上 key

掌握了這三個陣列方法,你就不再只是在「寫程式」,而是在「引導資料流動」。

核心複習:

  • map: 1 對 1 轉換,長度不變。
  • filter: 篩選,長度變小或不變。
  • reduce: 折疊成單一結果(數值、物件等)。
  • 不可變性: 它們都回傳新東西,這對 React 的狀態更新至關重要。

Topic 5 知識點回顧與整合

恭喜你!我們已經完成了 Topic 5: ES6+ 現代語法精要 的所有核心教學。

在這個主題中,我們從最基礎的解構賦值出發,學會了如何優雅地拆解 Props 與 State;接著深入探討了 Spread 與 Rest 運算子,理解了「淺複製」在不可變狀態更新中的重要性。我們也拆解了箭頭函數的 Lexical This 特性,這讓我們在 React 事件處理器中不再迷失方向。隨後,我們認識了 ES 模組系統 的靜態結構如何幫助現代工具進行效能優化(Tree Shaking),並學會了用 ?. (Optional Chaining)?? (Nullish Coalescing) 來防禦性地處理複雜的 API 資料。最後,我們透過本節的函數式陣列操作,將這一切知識串連起來,完成了從原始資料到 UI 呈現的最後一哩路。

這些語法不僅僅是為了讓程式碼變短,它們背後蘊含的「宣告式」、「不可變性」與「詞法作用域」思維,正是 React 框架設計的靈魂所在。掌握了這些,你已經具備了進入 React 核心原始碼與複雜架構的紮實基礎。

下一堂課,我們將正式跨入 Topic 6:React 核心設計哲學。我們將探討為什麼 React 選擇了這條路,Virtual DOM 的真面目是什麼,以及我們寫下的 JSX 程式碼到底是如何被轉換成瀏覽器看得懂的畫面的。