javaScript : 陣列宣告、種類、操作方法

 


想像你在整理一張待辦清單:有新增、刪除、重排、分組、搜尋。其實寫前端或 Node.js 也一樣,我們每天都在對陣列動刀。

只要工具用得對,清單就乖乖聽話;工具用錯,資料立刻亂套。

這篇不走艱深名詞,從最常見的宣告方式開始,帶你把 mapfilterreducetoSortedtoSpliced 等方法搞清楚,還會示範如何避開坑洞:例如 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]。


延伸閱讀推薦:



張貼留言 (0)
較新的 較舊