Learning Objectives
- 用 querySelectorAll 取得 NodeList,並使用 length、forEach、item、entries 等屬性與方法
- 說明 getElementsByTagName、getElementsByClassName、children 回傳的 HTMLCollection,以及 children 與 childNodes 的差別
- 分辨動態集合(live)與靜態集合(static):DOM 改變之後,哪一種集合會跟著更新
- 說明 NodeList 與 HTMLCollection 是類陣列,並用 Array.from、展開運算子轉成陣列後使用 map、filter
一次取得一群元素
網頁上常常有一整群相同的元素:清單的每一項、表格的每一列、一排商品卡片。
querySelectorAll、getElementsByTagName 這類方法一次取得全部,回傳的是一個集合。
集合看起來很像陣列:有 length,可以用 items[0] 取得第一個。但它不是陣列,
這一課要弄清楚三件事:
- 集合有兩種:NodeList 與 HTMLCollection,各有不同的方法
- 有些集合會隨著 DOM 改變自動更新(動態),有些不會(靜態)
- 想用陣列的
map、filter,要先把集合轉成真正的陣列
querySelectorAll 與 NodeList
document.querySelectorAll(選擇器) 回傳所有符合 CSS 選擇器的元素,結果是 NodeList。
選擇器可以用逗號分隔好幾個,例如 querySelectorAll('div.note, div.alert')。找不到時回傳長度 0 的 NodeList(不是 null)。
| NodeList 的屬性與方法 | 作用 |
|---|---|
length |
節點的數量 |
item(i) |
第 i 個節點,等同 list[i] |
forEach(fn) |
逐一處理每個節點,用法與陣列的 forEach 相同 |
entries() |
逐一取得 [索引, 節點] |
keys()/values() |
逐一取得索引/節點 |
NodeList 也可以直接放進 for...of 迴圈:for (const li of items) { … }。
HTMLCollection 與 children
getElementsByTagName、getElementsByClassName,以及元素的 children 屬性,回傳的是 HTMLCollection
(投影片把 getElementsByName 也列在這裡;依現行規格,它回傳的是會自動更新的 NodeList):
| HTMLCollection 的屬性與方法 | 作用 |
|---|---|
length |
元素的數量 |
item(i) |
第 i 個元素 |
namedItem('email') |
id 或 name 是 email 的元素 |
children 與 childNodes:兩者都是「子節點」,差別在包含什麼:
| 屬性 | 回傳 | 包含 |
|---|---|---|
el.children |
HTMLCollection | 只有元素節點 |
el.childNodes |
NodeList | 所有節點,包含文字節點(標籤之間的換行與空白也算)、註解節點 |
兩種集合的比較:
| HTMLCollection | NodeList | |
|---|---|---|
| 怎麼取得 | getElementsByTagName、getElementsByClassName、children |
querySelectorAll、childNodes、getElementsByName |
| 內容 | 只有元素 | 可能有各種節點 |
| 方法 | item、namedItem |
item、forEach、entries、keys、values |
| DOM 改變之後 | 動態:自動更新 | querySelectorAll 的結果是靜態,不會更新;childNodes 是動態 |
動態集合(live collection)像一扇窗,隨時反映 DOM 現在的樣子;靜態集合(static collection)像一張照片,
停在取得的那一刻。querySelectorAll 屬於 W3C 的 Selectors API 規範,回傳的就是靜態 NodeList。
想一想:NodeList 與 HTMLCollection
ul 原本有 2 個 <li>。執行下面的程式後,a.length 與 b.length 分別是多少?
const a = ul.children;
const b = ul.querySelectorAll('li');
ul.append(document.createElement('li'));
HTML 是下面這樣,box.children.length 與 box.childNodes.length 分別是多少?
<div id="box">
<p>一</p>
</div>
下列哪一個方法回傳的是 HTMLCollection?
NodeList 不是陣列
NodeList 與 HTMLCollection 都是類陣列(array-like):有編號的項目與 length,但不是真正的陣列。
Array.isArray(items) 是 false,陣列的 push、pop、map、filter、reduce 都不能用
(NodeList 只多了 forEach;HTMLCollection 連 forEach 都沒有)。
想用陣列的方法,先把集合轉成陣列:
const items = document.querySelectorAll('li');
const a = Array.from(items); // 最常用
const b = [...items]; // 展開運算子(集合必須可迭代)
const c = Array.prototype.slice.call(items); // 舊程式碼常見的寫法
const d = [];
for (const item of items) { d.push(item); } // 用 for...of 一個一個放進陣列
Array.from 還可以傳第二個參數,在轉換時順便處理每一項:Array.from(items, li => li.textContent)。
下面的範例用一個自己寫的類陣列物件示範(執行動畫在背景執行緒裡跑,沒有網頁可以取元素)。
NodeList 和它一樣有索引與 length;不同的是 NodeList 可以迭代,所以也能用展開運算子。
類陣列轉成陣列
fake 有 0、1、2 與 length,看起來像陣列,但 fake.map 是 undefined。注意第 7 行之後右邊多出的 arr1:它才是真正的陣列,可以用 map。最後的展開運算子會出錯,被 catch 接住。
想一想:類陣列
下面的程式會印出什麼?
const fake = { 0: 'a', 1: 'b', length: 2 };
console.log(Array.from(fake).length, typeof fake.forEach);
const items = document.querySelectorAll('li'),下列哪一個寫法會出現 TypeError?
document.getElementsByTagName('p').forEach(p => p.remove()) 會怎樣?
實作:轉成陣列再篩選
scores 是一個類陣列物件,就像從網頁取得的集合一樣不能直接用 filter。請把它轉成陣列,篩出 60 分以上的分數,依序印出及格的分數陣列與及格人數。
重點精華回顧
querySelectorAll回傳 NodeList,有length、item、forEach、entries、keys、values,也可以用for...of。getElementsByTagName、getElementsByClassName、children回傳 HTMLCollection,只有元素節點,方法只有item與namedItem。children只包含元素;childNodes包含所有節點,標籤之間的換行與空白也是文字節點。- HTMLCollection 與
childNodes是動態集合,DOM 改變會自動反映;querySelectorAll的 NodeList 是靜態集合,停在取得的那一刻。 - NodeList 與 HTMLCollection 是類陣列,不是陣列:沒有
map、filter、push。 - 用
Array.from(集合)、[...集合]、Array.prototype.slice.call(集合)或for...of轉成陣列後,就能使用陣列的所有方法。