
延續昨天介紹的 forEach 函式,今天要來介紹 map 和 reduce 這兩個函數式程式設計的核心工具~
map:陣列的變形工廠map 是函數式程式設計中最常用的工具之一。它的核心任務非常明確:對一個陣列中的每一個元素執行相同的操作,然後回傳一個包含所有結果的全新陣列。
map?假設我們有一個儲存商品價格的陣列,現在需要根據不同的業務需求,對這組價格進行多種轉換:
5% 營業稅後的價格。"$120.00"。如果用 for 迴圈來處理,程式碼可能會是這樣:
const prices = [20, 55, 120];
// 需求 1: 計算含稅價
const pricesWithTax = [];
for (let i = 0; i < prices.length; i++) {
  pricesWithTax.push(prices[i] * 1.05);
}
// console.log(pricesWithTax); -> [21, 57.75, 126]
// 需求 2: 計算折扣價
const discountedPrices = [];
for (let i = 0; i < prices.length; i++) {
  discountedPrices.push(prices[i] * 0.9);
}
// console.log(discountedPrices); -> [18, 49.5, 108]
// 需求 3: 格式化價格字串
const formattedPrices = [];
for (let i = 0; i < prices.length; i++) {
  formattedPrices.push(`$${prices[i].toFixed(2)}`);
}
// console.log(formattedPrices); -> ["$20.00", "$55.00", "$120.00"]
上面程式碼中,有個很常見的重複問題,三個 for 迴圈的處理流程幾乎一模一樣:
for 迴圈遍歷原始陣列push 進新陣列唯一不同的是迴圈內部那一行核心的「轉換邏輯」。
這種重複乍看之下沒什麼問題,程式也可以運作正常,但它有一點隱含的問題在於,它讓程式碼變得冗長,而且如果未來迭代的邏輯需要修改(例如,需要處理 null 或 undefined 的情況),我們就必須在三個地方同步修改,增加了維護成本和出錯的風險。
那要如何解決呢?  這痛點就是 map 要解決的問題,map 可以將這個重複的「迭代模式」抽象出來,變成可重複使用的工具,每次我們需要類似迴圈處理流程時,只需提供那段獨特的「轉換邏輯」即可。
map 函式現在我們來寫一個 map 函式來處理這段重複的邏輯,一個通用的 map 函式需要:
根據這個邏輯,我們可以寫出如下的程式碼:
function customMap(array, transform) { // 1. 接收一個陣列和轉換函數作為參數
  const newArray =; // 2. 建立一個新的空陣列
  
  for (let i = 0; i < array.length; i++) { // 3. 遍歷陣列
    const originalElement = array[i];
    const transformedElement = transform(originalElement); // 4. 應用轉換函數
    newArray.push(transformedElement); // 5. 將結果推入新陣列
  }
  
  return newArray; // 6. 回傳新陣列
}
// --- 使用我們自製的 map ---
const prices = [20, 55, 120];
const pricesWithTax = customMap(prices, price => price * 1.05);
const discountedPrices = customMap(prices, price => price * 0.9);
const formattedPrices = customMap(prices, price => `$${price.toFixed(2)}`);
console.log(pricesWithTax);   // [21, 57.75, 126]
console.log(discountedPrices); // [18, 49.5, 108]
console.log(formattedPrices);  // ["$20.00", "$55.00", "$120.00"]
我們將「迭代的邏輯」(for 迴圈)與「要執行的行為」(transform 函數)分離開來。customMap 封裝了繁瑣的迴圈細節,讓我們能專注於定義轉換規則,程式碼變得既簡潔又易於維護。
Array.prototype.map() 方法JavaScript 有一個內建的 Array.prototype.map() 方法,其用途就和我們自己手寫的 customMap 類似,此 map 方法會遍歷呼叫它的陣列中的每個元素,為每個元素呼叫一次我們提供的回呼函數 (callback function),並將每次回呼函數執行的結果蒐集起來,組成一個新的陣列回傳 。
這裡有兩個要注意的特性:
map 產生的新陣列,其長度必定與原始陣列完全相同。如果原始陣列有 10 個元素,map 之後的新陣列也絕對是 10 個元素。它像一個變形工廠的生產線,每個原料進去,就有一個成品出來。如下示意圖,map 函式會把輸入陣列(X)轉成輸出陣列(Y),因此我們需傳入能將 X 元素指向 Y 元素的函式(此轉換函式接受 X 元素,傳回 Y 元素)。
圖 1 map 運作示意圖(資料來源: 自行繪製)
map 不會修改 (mutate) 原始陣列。它會回傳一個全新的陣列,而原始陣列保持原封不動。這是函數式程式設計的核心原則之一,它能避免許多意想不到的副作用 (side effects),讓程式碼的行為更可預測。而為了維持不可變性,傳入 map 的函式應盡量為 Calculation,呼叫 map 的函式才會是 Calculation。舉個例子,假設我們有一個數字陣列,想要計算每個數字的平方根:
const numbers = [4, 9, 16, 25];
// 傳遞 Math.sqrt 這個函數給 map
const roots = numbers.map(Math.sqrt);
console.log(roots);   // 輸出: [2, 3, 4, 5]
console.log(numbers); // 輸出: [4, 9, 16, 25] (原始陣列安然無恙)
可以從上述程式中看出,原始的陣列 numbers 沒有被改變,並且輸出的陣列長度 roots 也和原始陣列 numbers 長度相同 👍
reduce:陣列聚合器如果說 map 是陣列的變形工廠,那麼 reduce 就是陣列的聚合器。它可以將一個陣列中的所有元素「濃縮」、「聚合」或「歸納」成一個單一的值。這個「單一的值」可以是任何東西:一個數字、一個字串、一個物件,甚至是一個新的陣列。
reduce?與 map 類似,我們先從一個情境出發。假設我們有一個購物車陣列,裡面包含了多個商品物件,每個物件有價格和數量。現在我們需要完成兩個任務:
使用 for 迴圈,我們會這樣做:
const cartItems = [
  { name: '鍵盤', price: 800, quantity: 1 },
  { name: '滑鼠', price: 1200, quantity: 1 },
  { name: '螢幕', price: 9500, quantity: 2 }
];
// 任務 1: 計算總金額
let totalCost = 0;
for (let i = 0; i < cartItems.length; i++) {
  totalCost += cartItems[i].price * cartItems[i].quantity;
}
console.log(totalCost); // 21000
// 任務 2: 計算總數量
let totalItems = 0;
for (let i = 0; i < cartItems.length; i++) {
  totalItems += cartItems[i].quantity;
}
console.log(totalItems); // 4
同樣地,我們看到了重複的模式。兩個迴圈都在做同一件事:
這個「從一個初始值開始,遍歷一個列表,並將其濃縮成一個最終值」的過程,就是「化簡」或「歸納」(Reduction)。
如果我們使用一個 reduce 函式,就可以將這個通用的「化簡」模式也抽象出來,我們只需要提供「如何更新累積值」的邏輯,不須管迴圈遍歷的細節。
reduce 函式我們可以打造一個 customReduce 函數來封裝這個重複的模式。它需要接收一個陣列、一個「化簡器」函數 (reducer) 和一個初始值 (initial value)。
function customReduce(array, reducer, initialValue) {
  let accumulator = initialValue; // 1. 從初始值開始
  
  for (let i = 0; i < array.length; i++) { // 2. 用索引遍歷陣列
    const element = array[i];
    // 3. 使用 reducer 函數更新 accumulator
    accumulator = reducer(accumulator, element);
  }
  
  return accumulator; // 4. 回傳最終的累積結果
}
// --- 使用我們自製的 reduce ---
const cartItems = [
  { name: '鍵盤', price: 800, quantity: 1 },
  { name: '滑鼠', price: 1200, quantity: 1 },
  { name: '螢幕', price: 9500, quantity: 2 }
];
// 任務 1: 計算總金額
const totalCost = customReduce(
  cartItems,
  (sum, item) => sum + (item.price * item.quantity),
  0
);
console.log(totalCost); // 21000
// 任務 2: 計算總數量
const totalItems = customReduce(
  cartItems,
  (count, item) => count + item.quantity,
  0
);
console.log(totalItems); // 4
透過 customReduce,我們可以將迭代的程式碼分離出去。我們現在可以用一種更宣告式的方式來表達我們的意圖:「將 cartItems 陣列化簡成一個總金額」或「化簡成一個總數量」,而不用關心 for 迴圈的細節。
Array.prototype.reduce()JavaScript 有一個內建的 Array.prototype.reduce() 方法,其用途就和我們自己手寫的 customReduce 類似,它會對陣列中的每個元素執行一個由我們提供的「化簡器」(reducer) 函數,並將其結果逐項累積,最終回傳一個單一的結果值。
最常見的例子就是加總一個數字陣列:
const numbers = [10, 20, 20];
const sum = numbers.reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
});
console.log(sum); // 50
reduce 的關鍵元素:accumulator 與 initialValuereduce 有兩個核心參數:accumulator(累加器)和 initialValue(初始值)。這也是我一開始學 reduce 最困惑的地方。reduce 的完整語法是 arr.reduce(callbackFn, initialValue)。
callbackFn(accumulator, currentValue, currentIndex, array)
accumulator:累加器。它是上一次回呼函數呼叫所回傳的值。currentValue:陣列中目前正在處理的元素。initialValue (可選):作為第一次呼叫 callbackFn 時 accumulator 的初始值。如以下示意圖,reduce 會一邊走訪陣列,一邊「累進」其中元素,最後得出一個合併完成的值。
圖 2 reduce 運作示意圖(資料來源: 自行繪製)
一般來說會建議提供 initialValue,因為如果在空陣列上呼叫 reduce,有無 initialValue 會有結果上的差異:
initialValue,會直接拋出 TypeError。因為 reduce 無法從空陣列中獲取第一個元素作為初始的 accumulator。initialValue,reduce 會安全地直接回傳 initialValue,而不會執行回呼函數。因此一律提供 initialValue 能讓 reduce 的行為更加一致和可預測,特別是在處理可能為空的陣列時,可以避免執行時錯誤。
reduce() 可以做什麼?reduce 的威力體現在它能將陣列轉換成任何你想要的資料結構。除了簡單的加總,它在「資料塑形」方面也非常強大,以下舉幾個應用範例。
reduce 可以將一個扁平的陣列,根據某個屬性重新組織成一個巢狀的物件結構。
舉例來說,我們可以用來將學生列表按照他們的成績分組。
const students = [
  { name: 'Alice', grade: 'A' },
  { name: 'Bob', grade: 'B' },
  { name: 'Charlie', grade: 'A' },
  { name: 'David', grade: 'C' },
  { name: 'Eve', grade: 'B' }
];
const groupedByGrade = students.reduce((groups, student) => {
  const { grade } = student; // 取得學生的成績
  
  // 如果這個成績的分類還不存在,就建立一個空陣列
  if (!groups[grade]) {
    groups[grade] = [];
  }
  
  // 將當前學生推進對應的分類陣列中
  groups[grade].push(student);
  
  return groups;
}, {});
console.log(groupedByGrade);
/*
{
  A: [ { name: 'Alice', grade: 'A' }, { name: 'Charlie', grade: 'A' } ],
  B: [ { name: 'Bob', grade: 'B' }, { name: 'Eve', grade: 'B' } ],
  C: [ { name: 'David', grade: 'C' } ]
}
*/
這是 reduce 的典型應用場景:處理一個物件陣列,並根據其屬性進行計算。
const cartItems = [
  { name: '鍵盤', price: 800, quantity: 1 },
  { name: '滑鼠', price: 1200, quantity: 1 },
  { name: '螢幕', price: 9500, quantity: 2 }
];
const totalCost = cartItems.reduce((total, item) => {
  return total + (item.price * item.quantity);
}, 0); // 初始總價為 0
console.log(totalCost); // 21000 (800 + 1200 + 19000)
將一個二維陣列「攤平」成一維陣列,是 reduce 的另一個常見用途。
const nestedArray = [
  [1, 2],
  [3, 4],
  [5, 6]
];
const flatArray = nestedArray.reduce((flat, currentArray) => {
  return flat.concat(currentArray);
}, []); // 初始值是一個空陣列
console.log(flatArray); 
// [1, 2, 3, 4, 5, 6]
雖然現在 JavaScript 內建了更方便的 Array.prototype.flat() 方法,但透過 reduce 來實現有助於深入理解其運作原理。
從這些例子可以看出,reduce 是處理列表資料最底層、最通用的計算模式。實際上,陣列的 map 和 filter 方法本身都可以用 reduce 來實現 。
補充:不同語言的
reduce()可能用不同名稱。
不同名稱如:
fold()foldLeft()(代表處理陣列的方向)foldRight()(代表處理陣列的方向)
今天介紹了 map 和 reduce 這兩個 JavaScript 中的高階函式,兩者差異如下:
map 專門用於對陣列進行一對一的轉換,並產生一個全新的、長度相同的陣列。reduce 能夠將整個陣列聚合、濃縮成任何形式的單一結果。我們也見證了從指令式「如何做」到宣告式「做什麼」的思維轉變。這種轉變讓我們能夠撰寫出意圖更明確、更少錯誤、更易於推理的程式碼,接著會再介紹 Currying(柯里化),繼續更深入理解 FP 程式設計~