[分享] 迭代 DOM NodeList 的一些花式

Lastor
Code 隨筆放置場
7 min readAug 12, 2019

這次A22挑戰題, 學長姐精選作業中, 有學長姐使用了一個沒看過的方法,
將 Array-like 轉換成 Array 來進行 .forEach() 迭代。

稍微研究了一下, 來分享分享XD

之前在U59有介紹過, JS抓取回來的DOM資料, 如果是一個 “集合”,
那該集合呈現的形式會很像一個 Array, 但它又不是 Array。
因為它無法使用 .pop() 或是 .forEach() 之類的陣列方法, 所以它並不是 JS 的 Array。

這種很像 Array 卻又不是 Array 的玩意, 有個專有名詞叫做 Array-like(類陣列)。

雖然 Array-like 無法使用 .forEach() 之類的陣列方法進行迭代,
但是強大的 for-of 迴圈, 不僅可以迭代Array, Object, 甚至是 Array-like 也完全ok。

事實上, JS存在了一些可以把 Array-like 轉換成 真Array 的方法。
先來看一些比較具體的範例, 例如現在有一組這樣的單純的HTML:

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

先用JS將它存取下來, 並用 Chrome DevTools 來觀察它

const ul = document.querySelector('ul')
ul.children
// HTMLCollection(3) [li, li, li], __proto__: HTMLCollection
const li = document.querySelectorAll('ul li')
li
// NodeList(3) [li, li, li], __proto__: NodeList
const array = [0, 1, 2]
array
// (3) [0, 1, 2], __proto__: Array(0)

可以觀察到, DOM集合長的很像陣列, 但是它清楚寫著 NodeList 或是 HTMLCollection。
而 Array 則單純的會標示著 Array。
( “__proto__” 屬性要點開集合的下拉選單才看的到, 它標示的是集合的類別)

陣列方法只能夠作用在 “__proto__” 屬性標示為 Array 的物件上。

ul.children.forEach(i => i.innerHTML = 'a')
// TypeError: ul.children.forEach is not a function

原理是因為, forEach之類的陣列方法其實是從 Array 原型物件繼承來的方法。
是一個長這樣的東西

Array.prototype.forEach = function(略) { 略 }

但這篇不是要討論物件導向, 我想以後課程應該會詳解這一塊, 現在先看看就好XD
結論就是, 所以只要不是 Array Object, 就不能使用 Array 的 method。

for-of 並不是原型方法, 所以沒有這種限制, 它可以迭代Array-like。
但是for系列迴圈的痛點就是code很長, 寫久了很煩……
就有很多討厭for系列的人研發出各種花式, 就因為討厭for迴圈www

接下來就進入正題, 有哪些花式可以取代 for-of 來迭代 Array-like 呢?

Array.prototype.slice.call( Array-like )

這個似乎是ES6以前的舊方法, 利用 slice() 與 call() 的組合, 可以把 Array-like 轉換成 Array
於是, 就可以使用陣列方法了!!

// 單獨console, 可以觀察到噴回了一個真Array
Array.prototype.slice.call(li) // (3) [li, li, li]
// 也可以寫成這樣, 比較短
[].slice.call(li) // (3) [li, li, li]
// 可以用forEach迭代 li 了!!
[].slice.call(li)
.forEach(i => i.innerHTML = "a")

原理部分, google一下可以看到很多文章討論。
但我覺得那有點複雜, 建議可以把 [].slice.call() 這整塊都當成一個method來記, 比較不會把坑挖的太深。

Array.prototype.push.apply( target Array, source Array-like )

這個跟上面那個很類似, 但沒有上面那個好用, 可以了解一下有這種花式就好。
這個方法本身不會直接回噴 Array, 回噴的是 length。
它必須弄一個變數, 讓apply()操作。

// 得弄一個變數來接 apply() 處理的資料
const array = []
[].push.apply(array, li)
array // (3) [li, li, li]
// 可以用forEach了
array.forEach(i => i.innerHTML = "a")

Array.from( Array-like )

這是ES6以後才出現的新方法, 也是最上面提到 @sushi 學姊所使用的方法。
它跟 [].slice.call() 差不多, 寫起來卻優雅很多。

// 單獨concole觀察, 變成真Array了
Array.from(li) // (3) [li, li, li]
// 一樣, 可以用forEach了
Array.from(li)
.forEach(i => i.innerHTML = "a")

展開運算子 (…target)

U87裡面提過的展開運算子, 可以這樣用

// 直接展開就可以轉成真Array
[...li] // (3) [li, li, li]
// 再使用想用的陣列方法
[...li].forEach(i => i.innerHTML = "a")

這個方法也可以做Deep Clone就是

const array = [ {略}, {略}, {略} ]
const clone = array.map(i => { return {...i} })

最後稍微說明一下究竟怎樣的物件算是 Array-like。
我們知道, JS裡面除了基本值之外, 其實通通都是 Obj。

Array 的構成其實是這樣的↓

只要符合這種結構, 但 _proto_不是 Array 的物件的都是 Array-like。
例如NodeList↓

其中最關鍵的是length屬性, 只要有這個東西, Array.from() 之類的方法都可以工作。

// 手做一個Array-like
const obj = {
0: 'hi',
1: 'hello',
name: 'Tom',
length: 3
}
// 轉換它
Array.from(obj) // (3) ["hi", "hello", undefined]

因為明確的設定了 length: 3, 所以產生的 Array 會有3個欄位。
之後程式會去找有沒有 key 長得跟 index 一樣是序列號, 是的就會把值放進Array。
而 name: ‘Tom’ 由於不是 index 格式, 所以回傳了 undefined 。

也因為這樣的工作方式,
所以討厭for迴圈的人也基於這特性開發出了很類似 Ruby Times-do 迴圈的作法↓

* Ruby times迴圈
4.times do
puts "hi"
end
// JS版本
Array.from({ length: 4 })
.forEach( () => console.log('hi') )
// JS傳統for寫法
for (let i = 0; i < 4; i++) { console.log(hi) }

最後, 為什麼說這些是花式呢?
因為 for 系列迴圈的效能其實是最高的…… 而且速度快很多。
這些轉 Array 的方法, 都多了一個轉換過程, 效能會比 for 系列來的差是鐵定的。

這些方法比較不常見, 所以我認為是一種花式,
可以不要用, 但review別人code時, 起碼得看的懂他在幹嘛XD

參考: Array.from的妙用 — 泡泡 — SegmentFault 思否

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。