Live NodeListとStatic NodeListの違いを調べた。一次情報として、MDNのNodeListのページには、取得時以降にDOMツリー内の変更がコレクションの内容に反映されるかされないかである、という説明があった。しかし、個人的な経験則に反する気がして落ち着かない。そういえば、NodeList
とHTML Collection
との違いも曖昧である。この機会に調べてみることにした。
Live NodeListとStatic NodeListの違い
まず、把握しているLive NodeListとStatic NodeListの違いを整理する。
項目/タイプ | Live | Static |
---|---|---|
取得方法 | document.getElementsByTagName() Node.childNodes |
document.querySelectorAll() |
取得時以降の DOM操作の影響 |
受ける | 受けない |
保持するプロパティやメソッドは、Live NodeListもStatic NodeListも変わらない。
以上である。整理するほどの情報でもなかった。
気になるのは、Static NodeListは、DOM操作の影響を受けないという点である。ブラウザベンダがそう言っている以上、疑問を挟む余地はない。ただ、その範囲がよくわからない。経験上、Static NodeListであっても、各ノードの子要素にはDOM操作による変更が反映されていたように思う(私のドドメ色の脳細胞が幻覚を見せていない限りは)。
どこからどこまでがStaticなのか知りたい。
DOM操作してStaticがどうなるか検証
盲滅法にあれこれ試して時間を無駄にした結果、以下のような方法で確認すれば良いと思いついた。
- 子ノードを持つ要素を1つ用意する。
- その要素を、LiveとStatic、それぞれで取得する。
- 要素の
innerHTML
を1文字ずつ消していき、各NodeListの中身を見る。
以下のデモがそれを形にしたものである。
「この文章はやがてpタグごと消滅する」は<p>
タグで、このうちの青字の部分は<span>
タグである。
「実験スタート」ボタンを押すと、<p>
タグの中身(innerHTML
)が1秒ごとに削られていく。文字がすべて削除されるのと同時に、<p>
は消滅する。その後、新たに<p>
を作り、ドキュメントに追加する。これが上記プログラムの大まかな概要である。
結果、StaticもLiveも、NodeList[0].innerHTML
は<p>
タグの操作を反映して削られていった。また、<p>
タグ内の<span>
タグが壊れた時点(青字が黒字になる時点)で、両方ともNodeList[0].children.length
が0になった。
<p>
タグが削除されると、Live NodeListは長さが0になるのに対し、Static NodeListは1のままである。<p>
タグが新たに生成されると、初期化してもいないのに、Live NodeListには生成した要素(ノード)の内容が反映される。対してStatic NodeListには変化がない。
Liveは定点カメラ映像、Staticはスナップショット
上記の事実から、Live NodeListは、要素のリストというより、DOMツリーの定点カメラのような役割を果たしていることがわかる。まさにLiveである。中身がゼロになっても、NodeListが生成された時点の条件に適う要素がDOMツリーに追加されたら、その参照を取り込むことができる。NodeListとは別に、そういうオブザーバーが生成されるようだ(これはChatGPT-4に聞いた)。
一方、Static NodeListの挙動はスナップショットを思わせる。生成時点で、各ノードをシャローコピーしてリストを作る。各プロパティへの参照は保持されるが、それを保持するのは別のNode
オブジェクトである。コピー元のノードのプロパティが変更されると、その変更はStatic NodeListに反映される。
嘘。
ドヤ顔でChatGPT-4に確認したら思いっきり間違っていた。すごく恥ずかしい。Static NodeListは、Live NodeListと同じく各Element
オブジェクトへの参照を直接保持する。ただ、ドキュメントからその要素が削除された後も、その要素への参照を保持し続ける。
これこそ嘘っぽいなと思ったら真実だった。以下のコードで確かめた。
上記の通り、Element.remove()
しても、static NodeListでNode
オブジェクトを保持していれば復活できる。
Live NodeListと違い、同条件のノードが新たにドキュメントに追加されても、Static NodeListは影響を受けない。同時に、保持しているノードがドキュメントから削除されても、Static NodeListは影響を受けない。一度捕まえた参照を決して逃さず、メモリ上にガチホし続ける。とんでもない握力である。
なぜ2種類のNodeListがあるのか
よくわからない。歴史的な問題、パフォーマンスの問題、ユースケースの問題、いろいろ絡み合っていそうである(詳しい方がいれば教えていただきたい)。
DOMツリーの変化を追う必要がなければ、Static NodeListを使っておけば良い、とだけ覚えておくことにする。
HTMLCollectionは歴史的遺物?
NodeList
と似たオブジェクトに、HTMLCollection
がある。しかし、両者は似て非なるものである。わかりやすいところではNodeList
はforEach()
メソッドが使えるが、HTMLCollection
は使えない。
ただ、もともとNodeList
にはforEach()
メソッドはなかった。ECMAScript 2015 (ES6) で大量の配列メソッドが追加された際にサポートするようになった。名前と機能が同じだけで、Array.forEach()
とは別物だが、それならHTMLCollection
にforEach()
を持たせても良い気がする。
なぜHTMLCollection
は拡張されなかったのか。謎だったが、MDNのHTMLCollectionのページに普通に書いてあった。
This interface was an attempt to create an unmodifiable list and only continues to be supported to not break code that’s already using it. Modern APIs use types that wrap around ECMAScript array types instead, so you can treat them like ECMAScript arrays, and at the same time impose additional semantics on their usage (such as making their items read-only).
訳:このインターフェイスは、変更不可能なリストを作成するための試みであり、既にそれを使用しているコードを壊さないためにサポートされ続けているだけである。最近のAPIでは、代わりにECMAScriptの配列型をラップする型を使用しているため、ECMAScriptの配列のように扱うことができ、同時にその使用方法に追加のセマンティクスを課すことができます(アイテムを読み取り専用にするなど)。
最近のAPIというのは、例えばNodeList
のentries()
やforEacy()
、keys()
などを指していると思われる。
また、WHATWGのHTML Collectionの項に、以下のような記述を見つけた。
HTMLCollection is a historical artifact we cannot rid the web of. While developers are of course welcome to keep using it, new API standard designers ought not to use it (use sequence<T> in IDL instead).
訳:HTMLCollectionは、ウェブから取り除くことのできない歴史的な遺物である。開発者がこれを使い続けることはもちろん歓迎されるが、新しいAPI標準の設計者はこれを使うべきではない(代わりにIDLでsequence>T<>を使う)。
HTMLCollection
は、積極的に排除されないまでも、どうやらはみだし者である。シンパシーを感じないでもない。