hogashi.*

日記から何から

querySelectorAllの結果をmapしたいときはArray.fromすると良い

 こんにちは〜 はてなエンジニア - Qiita Advent Calendar 2024 - Qiita の 1日目です。

querySelectorAllの結果をmapしたいとき

 JavaScript で、 Document: querySelectorAll() メソッド の結果を map したいことがある。が、そのままだと map できない。
 Chrome の devtools で試すとこう↓。 map は関数ではないです、というエラーになっている。

document.querySelectorAll('div').map(div => div.innerText)
// Uncaught TypeError: document.querySelectorAll(...).map is not a function
//    at <anonymous>:1:34
querySelectorAllの結果をそのままmapしようとしてエラーになっている様子

 map するには、 Array.from() を使って、 Array インスタンスにしてあげると良い。

Array.from(document.querySelectorAll('div')).map(div => div.innerText)
//=> ['hoge', ...] (innerText が入った配列が返ってくる)
querySelectorAllの結果をArray.fromしたのでmapできる様子

くわしく

 querySelectorAll の戻り値は NodeList というオブジェクト。

DocumentquerySelectorAll() メソッドは、指定された CSS セレクターに一致する文書中の要素のリストを示す静的な(生きていない)NodeList を返します。

https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll

 NodeList に map メソッドが存在しないので、そのまま map しようとすると冒頭のエラーになる。 MDN の NodeList のページには、まさに Array.from を使うといい、と書かれている。

メモ: NodeListArray とは異なりますが、forEach() メソッドで処理を反復適用することは可能です。Array.from() を使うことで Array に変換することができます。

https://developer.mozilla.org/ja/docs/Web/API/NodeList
ちなみに"静的な(生きていない)NodeList"とは

 生きた NodeList と生きていない NodeList というのは、 DOM の変化に応じて、自動的に更新されるかどうか、という話題。 NodeList - Web API | MDN に詳しい。
 querySelectorAll の戻り値は生きていないほうなので、取得して変数に格納したオブジェクトの内容が変化しない。逆に、 Node: childNodes プロパティ は生きた NodeList なので、 DOM が変化すると、その内容も変わりうる。

 試しに、 body タグの childNodes を変数に格納してから、 body タグに div タグをひとつ増やしてみた。変数に格納しておいたオブジェクトが変化していることがわかる。

child = document.body.childNodes
//=> NodeList(5) [script, div#root, script#hydration, div#a11y-status-message, div.ReactModalPortal]
document.body.insertAdjacentHTML('beforeend', '<div>hello!</div>')
//=> undefined
child
//=> NodeList(6) [script, div#root, script#hydration, div#a11y-status-message, div.ReactModalPortal, div]
childNodes(を変数に格納したオブジェクト)が変化している様子

 querySelectorAll に似たメソッド群で Document: getElementsByClassName() メソッド などは、生きた HTMLCollection が戻り値になっている。 querySelectorAll と同じ気持ちでコードを書いているとハマることがありそう……。

list = document.getElementsByClassName('browsers')
//=> HTMLCollection [div.browsers]
document.body.insertAdjacentHTML('beforeend', '<div class="browsers">Hi!</div>')
//=> undefined
list
//=> HTMLCollection(2) [div.browsers, div.browsers]
getElementsByClassNameの結果(を変数に格納したオブジェクト)が変化している様子