想像你在整理一張待辦清單:有新增、刪除、重排、分組、搜尋。其實寫前端或 Node.js 也一樣,我們每天都在對陣列動刀。
只要工具用得對,清單就乖乖聽話;工具用錯,資料立刻亂套。
這篇不走艱深名詞,從最常見的宣告方式開始,帶你把
map、filter、reduce、toSorted、toSpliced
等方法搞清楚,還會示範如何避開坑洞:例如
for...in 不適合走陣列、indexOf(NaN)
找不到東西、邊迭代邊
splice 會漏資料。
最後附上實戰步驟,把「取得資料 → 過濾 → 排序 → 分頁 → 渲染」串起來。讀完你會對陣列更有手感,改需求也不慌。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼要在意「陣列」
幾乎所有前端與 Node.js 後端程式,都離不開「一串資料」的處理:購物車商品清單、API 回傳的貼文列表、圖表的數值序列、表單驗證訊息……這些「可排序、可迭代」的集合,在 JavaScript 裡的第一主角就是 Array。
懂陣列,你能更穩、更快地寫出乾淨的資料邏輯;不熟陣列,Bug 常常就藏在迴圈、索引、或是方法誤用裡。
陣列是什麼?快速定義與核心特性
本質:陣列是特殊的物件(typeof [] === "object"),以「整數索引鍵」存放元素,維護一個 length 屬性。
有序集合:元素有索引順序(0 開始),可隨時新增、移除、改動。
動態長度:length 會隨新增/刪除變動,也可手動設定截斷。
可迭代:支援 for...of、展開運算子(...)、大多數高階方法(map、filter、reduce…)。
JavaScript 陣列
嚴格說 JS 只有一種標準陣列型別 Array,但在實務上常見下列「陣列家族」或近親類型,處理策略略有不同:
1. 一般陣列(Array)
最常用:[] 字面值、Array.of()、Array.from()、new Array()。
2. 巢狀/多維陣列(Nested / Multidimensional)
例如矩陣或樹狀資料:const grid = [[1,2],[3,4]];
常搭配 flat()、flatMap() 或遞迴處理。
3. 稀疏陣列(Sparse Array)
索引不連續、有「洞」:const a = []; a[10] = 1;(索引 0~9 為空洞)
迭代、length 與方法行為會很微妙,通常 避免。
4. TypedArray(型別化陣列)
像 Uint8Array、Float32Array,用於二進位/高效數值計算。
語法相似,但不是 Array,方法也不盡相同(沒有 map/filter 的可變版本,需特別留意)。
5. Array-like(類陣列)
例如 arguments、NodeList、HTMLCollection。
有 length 和索引,但不是 Array,常用 Array.from() 或 [...x] 轉換。
小結:日常 90% 情境用一般陣列即可;接觸 DOM、Web API、或效能/二進位時,才會遇到 Array-like、TypedArray。
宣告與建立陣列的方式
1. 字面值(最推薦)
const nums = [1, 2, 3];
const mix = [1, "a", true, { id: 1 }, [9, 8]];
清楚、直觀、可讀性最好。
2. Array.of(...items):避免 new Array(n) 歧義
Array.of(3); // [3] new Array(3); // 建立長度為 3 的稀疏陣列(有洞),不是 [3]
規則:若要用參數值建立陣列,選 Array.of()。
3. Array.from(iterableOrArrayLike, mapFn?)
const fromSet = Array.from(new Set([1,2,2,3])); // [1,2,3]
const doubled = Array.from([1,2,3], x => x*2); // [2,4,6]
const fromStr = Array.from("hi"); // ["h","i"]
把可迭代或「類陣列」轉成真正的陣列,必要技能。
4. 展開運算子(spread)
const copy = [...nums]; const merged = [...a, ...b]; const chars = [..."hello"]; // ["h","e","l","l","o"]
5. 其他:new Array(n) + fill、建表技巧
const fiveZeros = new Array(5).fill(0); // [0,0,0,0,0]
const table = Array.from({length: 3}, (_, i) => Array(4).fill(i));
// [
// [0,0,0,0],
// [1,1,1,1],
// [2,2,2,2]
// ]
雷點:new Array(5).map(...) 不會生效,因為是稀疏陣列,要先 fill 或用 Array.from。
讀寫與基本操作
讀寫元素
const arr = ["a","b","c"]; arr[0]; // "a" arr[1] = "B"; arr.length; // 3
動態長度
const a = [1,2,3,4,5]; a.length = 3; // 直接截斷 -> [1,2,3] a.length = 7; // 變稀疏,有洞 -> [1,2,3, <4 empty items>]
不建議用放大長度製造空洞。
新增/刪除尾端元素
arr.push("x"); // 回傳新長度
arr.pop(); // 回傳被移除的元素
新增/刪除開頭元素
arr.unshift("x");
arr.shift();
開頭操作會重排索引,大陣列效能較差。
方法大全(照「是否改動原陣列」分組)
A. 不改動原陣列(推薦在多人協作時優先)
複製/擴充:slice(), 展開 [...], concat()
查找:includes(), indexOf(), lastIndexOf(), find(), findIndex(), findLast(), findLastIndex()
轉換:map(), filter(), flat(), flatMap()
摺疊:reduce(), reduceRight()
排序(不變異版,現代瀏覽器/執行環境提供):
toSorted(compareFn?), toReversed(), toSpliced(start, deleteCount, ...items)
其他:join(), toString(), every(), some()
範例
const data = [3, 1, 20, 4]; // 不動到 data const sorted = data.toSorted((a, b) => a - b); // [1,3,4,20] const top3 = sorted.slice(0, 3); // [1,3,4] const even = data.filter(x => x % 2 === 0); // [20,4]
B. 會改動原陣列(變異方法)
插刪:splice()
排序與反轉:sort(), reverse()
推/彈/頭加/頭減:push(), pop(), unshift(), shift()
填值:fill(), copyWithin()
範例
const a = [1,2,3,4]; a.splice(1, 2, 9, 9); // 從索引1刪2個,插入9,9 -> [1,9,9,4] a.sort((x, y) => y - x); // 由大到小,原地排序
團隊協作常建議優先用不變異方法,可讀性高、除錯容易;需要極致效能或刻意原地操作才用變異。
迭代方法與選用建議
迴圈選擇
遍歷值:for...of、arr.forEach(cb)、map/filter/reduce 等。
遍歷索引:for (let i = 0; i < arr.length; i++)、arr.entries() 搭配 for...of。
避免 for...in 來走陣列:它列舉可列舉屬性,包含自訂鍵、原型鏈上的東西,且順序不保證。
範例:同時要索引與值
for (const [i, v] of arr.entries()) {
console.log(i, v);
}
實務處理清單(CRUD、查找、分組、去重、排序)
1. 新增/更新/刪除(CRUD)
// 新增(不變異)
const addItem = (list, item) => [...list, item];
// 更新:把 id=2 的項目 name 改掉
const updateById = (list, id, patch) =>
list.map(it => it.id === id ? {...it, ...patch} : it);
// 刪除:移除 id=2
const removeById = (list, id) => list.filter(it => it.id !== id);
2. 查找與判斷存在
const hasId2 = list.some(it => it.id === 2); const found = list.find(it => it.id === 2); // 找到物件或 undefined const idx = list.findIndex(it => it.id === 2); // 找到索引或 -1
3. 去重(Distinct)
const uniq = xs => Array.from(new Set(xs));
uniq([3,3,1,2,1]); // [3,1,2]
// 以某欄位去重(保留第一個)
const uniqBy = (arr, key) => {
const seen = new Set();
return arr.filter(x => !seen.has(x[key]) && seen.add(x[key]));
};
4. 分組(Group)
// 以 category 分組
const groupBy = (arr, key) =>
arr.reduce((acc, cur) => {
const k = cur[key];
(acc[k] ??= []).push(cur);
return acc;
}, {});
5. 排序
// 依價格由低到高 const sorted = products.toSorted((a, b) => a.price - b.price); // 依名稱用字串排序(避免預設的字典序問題) const byName = users.toSorted((a, b) => a.name.localeCompare(b.name));
解構、其餘參數(rest)、與展開(spread)
const [first, second, ...rest] = [10, 20, 30, 40];
// first=10, second=20, rest=[30,40]
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1,2,3); // 6
const a = [1,2];
const b = [3,4];
const merged = [...a, ...b]; // [1,2,3,4]
字串與陣列的轉換
"1,2,3".split(","); // ["1","2","3"]
[1,2,3].join("-"); // "1-2-3"
Array.from("你好"); // ["你","好"]
[..."你好"]; // ["你","好"]
不可變(Immutable)實務與防止副作用
在 React、Vue 組件狀態,或多人協作時,偏好回傳新陣列。
深拷貝巢狀資料可用 structuredClone(obj)(現代環境)或 JSON.parse(JSON.stringify(obj))(有限制)。
const next = structuredClone(prev); next.items.push(newItem);
效能與大數據列表注意
開頭插入/刪除(shift/unshift)成本高;大量資料改用 尾端操作 或設計成不可變替換。
sort() 對大陣列成本高,考慮伺服器端排序或分頁。
稀疏陣列(有洞)在迭代與方法行為上不一致,避開。
需要高密度數值運算,用 TypedArray。
TypedArray 快速入門(如果你在做影像、音訊、WebGL)
const buf = new ArrayBuffer(8); // 8 bytes const view = new Uint16Array(buf); // 以 16-bit 檢視 view[0] = 500; view[1] = 1000; // 依型別自動裁切
與 Array 不同,大小固定、元素型別固定、效能穩定。
方法較少,若要轉一般陣列可用 Array.from(view)。
完整操作步驟:從 API 資料到畫面
情境:有一份 API 回傳的商品列表,要做「過濾-排序-分頁-渲染」。
步驟 1:防禦性轉換
const raw = await fetch("/api/products").then(r => r.json());
const list = Array.isArray(raw) ? raw : []; // 防呆
步驟 2:過濾(例如只看上架且庫存>0)
const filtered = list.filter(p => p.enabled && p.stock > 0);
步驟 3:排序(價格低到高)
const sorted = filtered.toSorted((a, b) => a.price - b.price);
步驟 4:分頁(每頁 10 筆,第 page 頁)
const pageSize = 10; const page = 2; // 第 2 頁(從 1 開始) const start = (page - 1) * pageSize; const pageItems = sorted.slice(start, start + pageSize);
步驟 5:渲染(假設在前端)
pageItems.forEach(renderProductCard);
以上流程完全使用「不變異」方法,可讀性、可測性較佳。
常見錯誤與雷點
1. 把 new Array(n) 當成 [n] 用
new Array(3) // 稀疏陣列(長度 3),不是 [3] Array.of(3) // ✅ 得到 [3]
2. Array(n).map(...) 沒動作
new Array(5).map((_, i) => i); // -> [empty × 5],因為是稀疏陣列
Array.from({length: 5}, (_, i) => i); // ✅ [0,1,2,3,4]
3. 用 for...in 走陣列(順序不保證、可能跑到自訂屬性)
for (const x of arr) { /* ✅ */ }
arr.forEach(x => { /* ✅ */ });
4. sort() 不給比較函式(字串字典序,數字會出事)
[1, 20, 3].sort(); // ["1","20","3"] [1, 20, 3].sort((a,b)=>a-b); // ✅ [1,3,20]
5. 混淆 slice 與 splice
slice(start, end?):不變異,取片段。
splice(start, deleteCount, ...items):變異,刪改插。
6. 用 ==/=== 比較陣列內容(其實是比參考)
[1,2] === [1,2]; // false,不同參考 JSON.stringify(a) === JSON.stringify(b); // 粗糙但可行(順序要一致)
7. 邊迭代邊 splice(索引位移導致跳漏/重複)
做刪除時,改用 filter 產生新陣列;
或從尾端往前 for 迴圈處理。
8. indexOf(NaN) 找不到 NaN
[NaN].indexOf(NaN); // -1 [NaN].includes(NaN); // ✅ true
9. 把物件當值複製,忘了淺拷貝/深拷貝
const a = [{id:1},{id:2}];
const b = [...a]; // 只淺拷貝,內部物件仍共享參考
const c = structuredClone(a); // ✅ 深拷貝
10. 誤用 delete arr[i] 造成空洞
const a = [1,2,3]; delete a[1]; // [1, <empty>, 3](稀疏) a.splice(1,1); // ✅ [1,3]
11. 在 React/Vue 直接變異導致狀態不更新
儘量用不變異做法:setItems(prev => prev.filter(...))、toSorted()、toSpliced()。
12. length 手動拉長(產生空洞)
盡量用 Array.from({length:n}) 或 fill 建立可迭代內容。
13. 誤會 const 陣列不可改
const 只鎖定參考,內容仍可改:arr.push(1) 是合法的。
真正不可改需配合不變異策略或 Object.freeze(淺層)。
實務小工具
1. 穩健的安全取值與預設
const toArray = x => Array.isArray(x) ? x : (x == null ? [] : [x]);
2. 安全的分頁切片
const paginate = (arr, page=1, size=10) => {
const start = Math.max(0, (page-1) * size);
return arr.slice(start, start + size);
};
3. 不變異插入/替換/刪除
const insertAt = (arr, idx, ...items) => arr.toSpliced(idx, 0, ...items); const replaceAt = (arr, idx, ...items) => arr.toSpliced(idx, 1, ...items); const removeAt = (arr, idx, count=1) => arr.toSpliced(idx, count);
4. 安全排序(字串用 localeCompare)
const byNumAsc = (a, b) => a - b; const byStrAsc = (a, b) => String(a).localeCompare(String(b)); const byKeyAsc = k => (a, b) => (a[k] ?? "").localeCompare(b[k] ?? "");
5. 深拷貝或去循環參考(進階)
// 現代環境 const deepCopy = x => structuredClone(x); // 備案(有函式/特殊型別限制) const deepCopyJSON = x => JSON.parse(JSON.stringify(x));
測試思維:如何確保你的陣列邏輯穩
固定輸入 -> 固定輸出:map/filter/reduce 很適合單元測試。
邊界條件:空陣列、只有一個元素、重複值、極端大數、null/undefined。
不可變保證:測試原陣列在操作後「未被改動」(對不變異函式)。
問題集
Q1:要淺拷貝還是深拷貝?
只有扁平值:淺拷貝(slice, [...arr])即可。
巢狀物件:深拷貝(structuredClone)比較保險。
Q2:我要把 NodeList 變陣列?
const nodes = document.querySelectorAll("li");
const arr = Array.from(nodes); // 或 [...nodes]
Q3:我要快篩重複項?
基本型別:Array.from(new Set(xs))。
物件依 key:用 uniqBy。
Q4:排序中文字?
localeCompare("zh-Hant") 更接近中文排序需求:
names.toSorted((a,b)=>a.localeCompare(b,"zh-Hant"));
整體協作的規則
1. 預設使用不變異方法(toSorted、toSpliced、map、filter、reduce)。
2. 避免稀疏陣列與 delete;必要時先填值或改資料模型。
3. 排序務必提供比較函式,數字/字串區分處理。
4. Array-like 先轉陣列 再做操作。
5. 大型資料 避免 shift/unshift;如需,考慮雙端佇列結構或重新設計流程。
6. 明確命名:list, items, filtered, sorted, pageItems,讀起來就知道每步做什麼。
結語
陣列是 JavaScript 的日常語言:把資料「排隊、篩選、轉換、分組、統計」的工具箱。當你熟練上述宣告方式、清楚變異與不變異的差異、會挑對迭代方法、避開稀疏陣列與排序陷阱,處理一串資料就會像呼吸一樣自然。遇到需求時回來檢查這份清單,從 「宣告 → 轉換 → 查找/篩選 → 排序 → 分頁/聚合 → 渲染」 一步步走,你的陣列邏輯會穩、可讀、也更能通過實務的檢驗。
延伸小抄
建立:[]、Array.of()、Array.from()、[...iterable]
查找:includes(含 NaN)、find、findIndex、some
轉換:map、filter、flat、flatMap、reduce
複製/切片:slice、concat、[...]
排序:toSorted / sort(compareFn)(務必給比較函式)
不可變插刪改:toSpliced、toReversed
避免:for...in、delete arr[i]、new Array(n).map、未給比較函式的 sort、把 new Array(n) 當 [n]。
