JavaScript 陣列操作:sort / reverse / flat / flatMap
簡介
在日常的前端開發中,陣列是最常見的資料結構之一。無論是從 API 取得的清單、表單輸入的多筆資料,或是 UI 元件需要的排序與分層顯示,都離不開對陣列的操作。
本單元聚焦在四個非常實用的陣列方法——sort、reverse、flat、flatMap。它們分別負責 排序、反轉、展平多層陣列,以及 映射後立即展平。掌握這四個方法,能讓你在處理資料時少寫大量迴圈與手動處理的程式碼,寫出更簡潔、更具可讀性的程式。
以下內容將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你深入了解這些 API,並提供實務應用的案例,幫助初學者到中階開發者快速上手。
核心概念
1. Array.prototype.sort() – 排序
sort 會 直接在原陣列上進行排序(即「mutate」),預設會把元素轉成字串後以 Unicode 編碼排序,這往往不是我們想要的「數值」排序結果。
為什麼需要比較函式?
提供一個 比較函式 (compareFn) 可以自訂排序規則。比較函式必須回傳:
| 回傳值 | 意義 |
|---|---|
| < 0 | a 排在 b 前面 |
| 0 | a 與 b 相等,順序不變 |
| > 0 | a 排在 b 後面 |
程式碼範例
// 1️⃣ 數值升冪(必須提供比較函式)
const numbers = [10, 2, 33, 4, 25];
numbers.sort((a, b) => a - b);
console.log(numbers); // [2, 4, 10, 25, 33]
// 2️⃣ 數值降冪
numbers.sort((a, b) => b - a);
console.log(numbers); // [33, 25, 10, 4, 2]
// 3️⃣ 依字母大小寫不敏感排序(localeCompare)
const fruits = ['Banana', 'apple', 'Orange', 'grape'];
fruits.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
console.log(fruits); // ['apple', 'Banana', 'grape', 'Orange']
// 4️⃣ 物件陣列依屬性排序
const users = [
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 22 },
{ name: 'Carol', age: 35 }
];
users.sort((x, y) => x.age - y.age);
console.log(users);
// [{name:'Bob',age:22},{name:'Alice',age:28},{name:'Carol',age:35}]
小提醒:
sort是 不穩定 的(在舊版 V8 中),但自 ES2019 起已成為穩定排序,相同鍵值的元素會保持原有相對順序。
2. Array.prototype.reverse() – 反轉
reverse 同樣是 原位(in‑place) 操作,會把陣列的順序顛倒。常與 sort 結合使用,快速取得 降冪 或 最新的項目在最前 的效果。
程式碼範例
// 1️⃣ 基本使用
const letters = ['a', 'b', 'c', 'd'];
letters.reverse();
console.log(letters); // ['d', 'c', 'b', 'a']
// 2️⃣ 先排序再反轉 → 取得降冪
const scores = [85, 42, 97, 63];
scores.sort((a, b) => a - b).reverse();
console.log(scores); // [97, 85, 63, 42]
// 3️⃣ 反轉字串(先 split 再 reverse 再 join)
const str = 'JavaScript';
const reversed = str.split('').reverse().join('');
console.log(reversed); // 'tpircSavaJ'
注意:
reverse會改變原陣列,若需要保留原始資料,請先slice()或使用展開運算子[...]產生副本。
3. Array.prototype.flat() – 展平(Flatten)
flat 用來把 多層巢狀陣列 拉平成一層(或指定深度)。默認深度為 1,若傳入 Infinity 則會把所有層級全部展平。
程式碼範例
// 1️⃣ 單層展平(預設深度 1)
const nested1 = [1, [2, 3], [4, [5]]];
console.log(nested1.flat()); // [1, 2, 3, 4, [5]]
// 2️⃣ 指定深度
console.log(nested1.flat(2)); // [1, 2, 3, 4, 5]
// 3️⃣ 完全展平
const deep = [1, [2, [3, [4, [5]]]]];
console.log(deep.flat(Infinity)); // [1, 2, 3, 4, 5]
// 4️⃣ 兼容稀疏陣列(空位會被移除)
const sparse = [1, , 2, [3, , 4]];
console.log(sparse.flat()); // [1, 2, 3, 4]
小技巧:
flat只在 ES2019+ 環境支援,舊版瀏覽器可用Array.prototype.concat.apply([], arr)或手寫遞迴函式代替。
4. Array.prototype.flatMap() – 映射後展平
flatMap 結合了 map + flat(1) 的功能,先對每個元素執行映射函式,再把結果展平一層。它比先 map 再 flat 更有效率,因為只遍歷一次陣列。
程式碼範例
// 1️⃣ 基本範例:將每個字串切割成字元陣列,並展平
const words = ['hello', 'world'];
const chars = words.flatMap(word => word.split(''));
console.log(chars); // ['h','e','l','l','o','w','o','r','l','d']
// 2️⃣ 產生「展開」的座標資料
const points = [{x:1,y:2}, {x:3,y:4}];
const coordinates = points.flatMap(p => [p.x, p.y]);
console.log(coordinates); // [1,2,3,4]
// 3️⃣ 過濾與展平:只保留偶數,並展平
const numbers2 = [1, 2, 3, 4, 5];
const evens = numbers2.flatMap(n => (n % 2 === 0 ? [n] : []));
console.log(evens); // [2,4]
// 4️⃣ 與物件結合:把每筆訂單的商品展平成單一列表
const orders = [
{ id: 101, items: ['apple', 'banana'] },
{ id: 102, items: ['orange'] },
];
const allItems = orders.flatMap(o => o.items);
console.log(allItems); // ['apple','banana','orange']
差異說明:如果你使用
map後接flat,會產生兩次迭代;flatMap只做一次迭代,效能上更佳,且語意更清晰。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的做法 |
|---|---|---|
| 陣列被原位改變 | sort、reverse、flat、flatMap(flat)皆會直接改變原陣列。 |
若需保留原始資料,先 const copy = [...arr]; 再操作。 |
sort 直接比較字串 |
["10","2"] 會得到 ["10","2"](字典序),而非數值升序。 |
使用比較函式 arr.sort((a,b)=>a-b);或先 map(Number)。 |
| 比較函式忘記回傳數值 | 若回傳 true/false,排序結果不可預期。 |
確保 compareFn 回傳 負數、0、正數。 |
flat 深度不夠 |
flat(1) 只會展平一層,深層仍保留。 |
依需求傳入正確的 depth,或 Infinity 完全展平。 |
| 稀疏陣列的空位 | flat 會把空位視為 undefined 並移除,可能導致資料遺失。 |
若需要保留位置,可先 map 為明確值或使用 Array.from。 |
flatMap 只能展平一層 |
flatMap 的展平深度固定為 1,無法一次展平多層。 |
需要多層展平時,先 map 再 flat(depth),或在 flatMap 內回傳已展平的子陣列。 |
| 使用舊版瀏覽器 | flat / flatMap 於 ES2019 前不支援。 |
使用 Polyfill 或 Babel 轉譯。 |
最佳實踐
- 永遠先考慮不變性(immutability):在函式式程式設計中,盡量避免直接改變傳入的陣列,使用
slice()、展開運算子或Array.from產生新陣列。 - 明確寫比較函式:即使是字串陣列,也建議使用
localeCompare,避免因 Unicode 排序差異產生錯誤。 - 結合
filter與flatMap:若需要同時過濾與展平,flatMap可以直接回傳空陣列做「過濾」的動作,省去額外的filter步驟。 - 使用
Infinity為深度時要小心記憶體:對超大或深度不確定的資料結構展平,可能產生巨大的陣列,需評估效能與記憶體限制。
實際應用場景
| 場景 | 可能使用的方法 | 為什麼適合 |
|---|---|---|
| 商品清單依價格排序 | sort((a,b)=>a.price-b.price) |
使用者常需要「由低到高」或「由高到低」的價格排列。 |
| 聊天訊息顯示最新在最上 | messages.sort((a,b)=>b.time - a.time) 或 messages.reverse()(若已是時間遞增) |
讓 UI 一次渲染即可呈現正確順序。 |
| 多層分類資料展平成選項列表 | categories.flat(depth) 或 categories.flat(Infinity) |
例如部落格的標籤、階層式目錄。 |
| 將 API 回傳的嵌套陣列轉成單一陣列 | response.flatMap(item => item.subItems) |
減少迴圈層次,直接得到所需的子項目集合。 |
| 文字搜尋的關鍵字切割 | input.split(/\s+/).flatMap(word => word.split('')) |
把使用者輸入的句子拆成字元或單詞,便於索引或高亮顯示。 |
| 生成座標點陣列供圖表繪製 | data.flatMap(d => [d.x, d.y]) |
一次取得所有 X、Y 值,直接傳給繪圖函式。 |
| 過濾並展平 API 回傳的錯誤訊息 | `errors.flatMap(e => e.messages |
總結
本篇文章從 排序 (sort)、反轉 (reverse)、展平 (flat)、映射後展平 (flatMap) 四個核心 API 出發,說明了它們的行為特性、使用方式與常見的陷阱。掌握這些方法,你可以:
- 快速整理資料:依需求排序、倒序或展平,使資料結構更符合前端顯示或後端傳遞的規格。
- 寫出更簡潔的程式:
flatMap讓映射與展平一步完成,減少迴圈與臨時陣列。 - 避免副作用:了解哪些方法會「原位」改變陣列,適時使用副本保護資料完整性。
在實務開發中,這些陣列操作常與 filter、map、reduce 搭配使用,形成強大的資料管線(pipeline)。只要遵循 不變性、明確比較、適度展平 的最佳實踐,你的程式碼將更具可讀性、維護性與效能。祝你在 JavaScript 陣列的世界裡玩得開心,寫出更優雅的程式!