JavaScript 陣列操作(Arrays)— 類陣列物件(Array‑like Objects)
簡介
在日常的 JavaScript 開發中,我們常會遇到「看起來像陣列」卻不是真正 Array 的資料結構。這類結構被稱為 類陣列物件(Array‑like Objects)。它們具備 length 屬性以及以數字索引存取的特性,讓我們可以用類似陣列的方式遍歷或取值,但卻缺少 Array.prototype 上的大部份方法(如 push、map、filter 等)。
了解類陣列物件的運作原理,能讓你在處理 arguments、NodeList、字串、以及自訂的類陣列結構 時,寫出更乾淨、效能更佳的程式碼。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握這個主題。
核心概念
什麼是類陣列物件?
類陣列物件具備以下兩個核心特徵:
length屬性:是一個非負整數,表示「可存取的元素個數」。- 數字索引:屬性名稱為
0、1、2…,可直接透過obj[0]方式取得值。
注意:類陣列物件 不繼承
Array.prototype,因此沒有forEach、map等方法。
常見的類陣列物件包括:
| 類型 | 來源 | 範例 |
|---|---|---|
arguments |
函式內部的參數集合 | function foo(){ console.log(arguments); } |
NodeList |
DOM 查詢結果 (document.querySelectorAll) |
document.querySelectorAll('li') |
HTMLCollection |
DOM 元素集合 (document.forms) |
document.forms |
String |
字串本身(可視為字元陣列) | "hello" |
| 自訂物件 | 手動模擬 length 與索引 |
let obj = {0:'a',1:'b', length:2} |
為什麼要把類陣列轉成真正的陣列?
雖然可以用 for 迴圈遍歷類陣列,但 陣列方法(map、filter、reduce 等)提供了更簡潔且具可讀性的寫法。將類陣列轉換為 Array,可以:
- 使用鏈式呼叫(chain)提升程式碼可讀性
- 利用 ES6+ 新語法(如
...spread)簡化資料處理 - 避免手動索引錯誤(如
i < obj.length時忘記i++)
常見的轉換技巧
1. Array.from()
ES6 原生提供的靜態方法,接受類陣列或可迭代物件,回傳新陣列。
// 例:把 arguments 轉成陣列
function sum() {
const args = Array.from(arguments); // => [1, 2, 3, ...]
return args.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
優點:語意清晰,可接受第二個映射函式,直接在轉換時完成
map。
2. 展開運算子(Spread Operator)
... 會先呼叫物件的 iterable 介面;對於 NodeList、arguments 也有效。
const nodeList = document.querySelectorAll('p');
const pArray = [...nodeList]; // 等同於 Array.from(nodeList)
pArray.forEach(p => p.style.color = 'tomato');
注意:若目標不是 iterable(例如自訂的類陣列),此方式會拋出
TypeError。
3. Array.prototype.slice.call()
在 ES5 時最常使用的技巧,透過 slice 產生淺拷貝。
function listToArray(list) {
// list 可能是 arguments、NodeList、HTMLCollection
return Array.prototype.slice.call(list);
}
// 範例:將 HTMLCollection 轉成陣列
const formsArray = listToArray(document.forms);
formsArray.forEach(form => console.log(form.id));
缺點:語意較不直觀,且在某些環境(如
TypedArray)會失效。
4. 手動迭代(for...of)
若類陣列同時具備 iterable(如 NodeList),可以直接使用 for...of,但仍需要轉成陣列才能使用陣列方法。
const args = [];
for (const arg of arguments) {
args.push(arg);
}
console.log(args);
程式碼範例
以下提供 4 個實務中常見的類陣列操作範例,說明從「取得」到「轉換」再到「使用」的完整流程。
範例 1️⃣:arguments 轉陣列 + map
function doubleAll() {
// 把 arguments 轉成陣列
const nums = Array.from(arguments);
// 使用 map 產生新陣列
return nums.map(n => n * 2);
}
console.log(doubleAll(1, 2, 3)); // [2, 4, 6]
說明:Array.from 直接把類陣列變成真正的陣列,接著使用 map 完成每個元素的倍增。
範例 2️⃣:NodeList → 展開運算子 → filter
// 取得所有 <li> 元素
const liNodes = document.querySelectorAll('li');
// 把 NodeList 轉成陣列,然後挑出含有 .active 的項目
const activeItems = [...liNodes].filter(li => li.classList.contains('active'));
console.log('Active <li> 數量:', activeItems.length);
說明:展開運算子 ... 可直接把 NodeList 變成陣列,之後使用 filter 取得符合條件的元素。
範例 3️⃣:自訂類陣列物件 → Array.from + 第二參數映射
// 手動建立類陣列物件
const pseudoArray = {
0: 'apple',
1: 'banana',
2: 'cherry',
length: 3
};
// 轉換時直接把字串轉成大寫
const fruits = Array.from(pseudoArray, item => item.toUpperCase());
console.log(fruits); // ["APPLE", "BANANA", "CHERRY"]
說明:Array.from 的第二個參數是 映射函式,讓你在轉換的同時完成資料處理,省去額外的 map 步驟。
範例 4️⃣:HTMLCollection → Array.prototype.slice.call → reduce
// 取得所有 <input> 元素(HTMLCollection)
const inputs = document.forms[0].elements;
// 把 HTMLCollection 轉成陣列
const inputArray = Array.prototype.slice.call(inputs);
// 計算所有文字框的總字元長度
const totalLength = inputArray
.filter(el => el.type === 'text')
.reduce((sum, el) => sum + el.value.length, 0);
console.log('文字框總字元數:', totalLength);
說明:在舊版瀏覽器(IE)中,Array.from 可能不支援,這時 slice.call 是相容性的備援方案。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 / 最佳實踐 |
|---|---|---|
忘記 length 不是自動更新 |
手動改變索引值後,未同步更新 length 會導致遍歷錯誤。 |
永遠 以 length 為基準,修改索引時同時調整 length,或使用 Array.from 直接產生新陣列。 |
使用 for...in 迭代 |
for...in 會遍歷原型鏈上的屬性,可能得到非索引鍵。 |
只用 for、for...of、Array.from 或 Array.prototype.forEach(在轉成陣列後)來遍歷。 |
| 類陣列不是 iterable | 自訂類陣列若缺少 [Symbol.iterator],...spread 會失效。 |
若需要支援展開運算子,實作 [Symbol.iterator],或直接使用 Array.from。 |
直接改寫 Array.prototype |
為類陣列掛上 Array 方法會污染全域,造成不可預期錯誤。 |
避免 為類陣列直接改寫原型,改以 工具函式(如 toArray)或 ES6 class 包裝。 |
| 舊版瀏覽器相容性 | Array.from、...spread 在 IE 不支援。 |
使用 polyfill(如 core-js)或 Array.prototype.slice.call 作為後備。 |
最佳實踐總結:
- 優先使用
Array.from:語意最清晰,且支援映射函式。 - 在需要兼容舊環境時,備用
slice.call。 - 若類陣列本身是 iterable,直接使用展開運算子,可寫得更簡潔。
- 避免直接在類陣列上呼叫
Array.prototype方法,而是先轉換。 - 為自訂類陣列實作
[Symbol.iterator],讓它在所有 ES6+ 場景下都能如陣列般使用。
實際應用場景
| 場景 | 為何會碰到類陣列 | 常用解法 |
|---|---|---|
函式參數不定長 (rest 之前) |
arguments 只能以類陣列形式取得所有參數。 |
Array.from(arguments) → map、filter 等。 |
| DOM 操作 | document.querySelectorAll、getElementsByClassName 回傳 NodeList / HTMLCollection。 |
const nodes = [...nodeList]; → nodes.forEach(...)。 |
| 表單序列化 | form.elements 為 HTMLCollection,需遍歷取得每個欄位值。 |
Array.from(form.elements).filter(...).reduce(...)。 |
| 第三方函式庫 | 某些舊版函式庫回傳類陣列(如 jQuery 的 $(selector) 內部的 jQuery 物件)。 |
Array.from($.makeArray(jQueryObj)) 或 [].slice.call(jQueryObj)。 |
| 自訂資料結構 | 為了效能或記憶體需求,手動建立「類陣列」來儲存大量資料。 | 為物件實作 [Symbol.iterator] → 直接使用 for...of 或 ...spread。 |
總結
類陣列物件是 JavaScript 中 介於純粹物件與正式陣列之間的特殊結構。掌握它們的特性與轉換技巧,能讓你在處理 函式參數、DOM 集合、或自訂資料結構 時,寫出更具可讀性與效能的程式碼。關鍵要點如下:
- 類陣列 ≠ 陣列:缺少原型方法,必須自行轉換。
Array.from是首選:語意明確,支援映射。- 展開運算子 (
...) 讓程式更簡潔,但需確保目標是 iterable。 slice.call為舊環境的可靠備援。- 避免
for...in、直接改寫原型或忘記同步length,這些是最常見的陷阱。
只要熟練上述技巧,你就能在日常開發中自如地處理各種「看起來像陣列」的資料,提升程式碼品質與維護性。祝你寫程式愉快,持續探索 JavaScript 的無限可能! 🚀