私にとってここ数年、JavaScriptでDOM操作が必要なサイトを制作するにあたってはjQueryかReactの二択しかありませんでした。例えば普通のコーポレートサイトのようなそれほど複雑なDOM操作は要求されないような場合はjQuery、それ以外のSPAのようなものの場合はReactを使うという感じです。まだ当面はこれで何とかなりそうな気もしますが、最近の「脱jQuery」の流れにも対応しておきたいな、と思っていたので「特定のフレームワーク・ライブラリに依存しないDOM操作」をテーマに、抑えておきたい基本的なことと、DOMの基本操作についてまとめました。
本記事では各種DOM操作用APIの使い方にも触れていますが、もちろん全てを網羅しているわけではありません。個人的にではありますが、現場で使用する可能性のありそうなもののみに絞っています。もし詳細を知りたい場合はMDN等を参考にしてください。
【「Vanilla JS」とは?】
本記事ではjQueryとの対比としてネイティブなJavaScriptのことを「Vanilla(ヴァニラ)JS」と呼んでいます。「Vanilla JS」はフレームワークありきで語られることの多いJavaScript界の風潮を皮肉ったジョークフレームワークで、要はフレームワークやライブラリに依存していない、素のJavaScriptのことを指します。
「素のJavaScript」「ネイティブのJavaScirpt」「Pure JavaScript」etc..
という言葉よりもVanilla JSの方が説明しやすかったので、「ネイティブJavaScript = Vanilla JS」として統一して解説しています。
要素の参照
jQueryの場合、DOM参照は次の例の様に「$」関数にセレクタを渡すだけで済みます。また、DOM参照・操作関数が返すオブジェクトは基本的にjQueryオブジェクトに統一されているため、メソッドチェーンで宣言的にDOM操作を行うことが可能です。
const $elms = $('selector1'); $elms.find('selector2') .css({color: 'red'}) .text('hoge') .show();
これに対し、Vanilla JSではDOM参照のための関数がいくつか用意されています。
// 要素のIDから参照 const elmById = document.getElementById('#selector'); // クラス名から参照 const elmsByClass = document.getElementsByClassName('className'); // 要素名(タグ名)から参照 const elmsByTag = document.getElementsByTagName('tagName'); // CSSセレクタで参照(単一) const elmByQuery = document.querySelector('selector'); // CSSセレクタで参照(集合) const elmsByQuery = document.querySelectorAll('selector');
DOMの取得方法一覧
上記のDOM参照用関数ではElement
, HTMLCollection
, NodeList
のいずれかを取得することができます。
Element
は単一の要素を表し、HTMLCollection
, NodeList
はElement
の集合を表します。
次の表はこれらの関数が返すオブジェクトのタイプを表したものです。
document.getElementById | Element |
---|---|
document.getElementsByClassName Element.getElementsByClassName |
HTMLCollection |
document.getElementsByTagName Element.getElementsByTagName |
HTMLCollection |
document.querySelector Element.querySelector |
Element |
document.querySelectorAll Element.querySelectorAll |
NodeList |
上記以外にも要素を取得するためのインターフェースはいくつかありますが、少なくとも現場レベルでは上に挙げたものを押さえておけば事足りるのではないかと思います。
また、上の表の内、getElementById
以外はdocument
からでも、Element
からでも呼び出すことが可能です。document
から呼び出した場合、要素の検索対象が文章全体になるのに対し、Element
から呼び出した場合は、そのElement
を子孫要素(サブツリー)が検索対象となります。
単一のElement
を返すメソッドで、該当Element
が複数ある場合は、ドキュメント内で最初に出現するElementを返します。
HTMLCollectionとNodeList
HTMLCollection
とNodeList
はどちらもElement
の集合を表しますが別物です。
どちらも配列(Array)に似ていますが、厳密には配列ではなくそれぞれ性質の異なる固有のオブジェクトです。そのため、配列操作で比較的よく使用されるメソッド(map や filter など)も持っていません。
HTMLCollection
とNodeList
とでは使用できるメソッド等の違いのほか、動的か静的かという性質の違いもあります。
HTMLCollection
は取得後もDOM
の変更を反映するため、「動的/生きている」という性質をもちますが、NodeList
は取得後のDOMの変更を反映しないので「静的」な性質を持ちます。
この性質の違いは次のコードで確認することができます。
<ul id="items"> <li>foo</li> <li>bar</li> </ul>
const items = document.getElementById('items'); const htmlCollection = document.getElementsByTagName('li'); const nodeList = document.querySelectorAll('li'); const newItem = document.createElement('li'); console.log(htmlCollection); // HTMLCollection(2) [li, li] console.log(nodeList); //NodeList(2) [li, li] newItem.textContent = 'baz'; items.appandChild(newItem); console.log(htmlCollection); // HTMLCollection(3) [li, li, li] console.log(nodeList); //NodeList(2) [li, li] // HTMLCollectionは要素の数が増えているが、NodeListは取得時のままとなる。
HTMLCollection, NodeList内の個々の要素にアクセスするには?
HTMLCollection
もNodeList
もどちらもElement
の集合を表すオブジェクトですが、格納されている個々の要素を取得するには次の様にします。いずれの場合も単一要素であるElement
を取得することができます。
<ul> <li name="foo">foo</li> <li>bar</li> <li>baz</li> </ul>
const elms = document.getElementsByClassName('li'); console.log(elms[0]); // <li name="foo">foo</li> console.log(elms.item(0)); // <li name="foo">foo</li> console.log(elms.namedItem('hoge')); // <li name="foo">foo</li>
const elms = document.querySelectorAll('li'); console.log(elms[0]); // <li name="foo">foo</li> console.log(elms.item(0)); // <li name="foo">foo</li>
HTMLCollection, NodeListのループ処理
HTMLCollection
、NodeList
はいずれもElement
の集合ですが厳密には配列ではありません。では格納されているElement
に対してループ処理を行うにはどのようにすればよいのでしょうか?
NodeList
には配列と同じようにforEach
メソッドがありますのでこれを使うことができます。
<ul> <li>foo</li> <li>bar</li> <li>baz</li> </ul>
const elms = document.querySelectorAll('li'); elms.forEach(elm => { // Elementオブジェクト console.log(elm); });
NodeList.forEach
はIEでは使用出来ません。また、HTMLCollection
にもforEach
メソッドがなく、代わりのメソッドもないため、代わりの手段を用いてループ処理を実装する必要があります。
次の例は代替手段の一例ですが、Array
オブジェクトのforEach
メソッドを拝借する形で実装しています
const htmlCollection = document.getElementsByTagName('li'); const nodeList = document.querySelectorAll('li'); Array.prototype.forEach.call(nodeList, elm => { // Elementオブジェクト console.log(elm); }); Array.prototype.forEach.call(htmlCollection, elm => { // Elementオブジェクト console.log(elm); });
親子兄弟要素の取得
ある要素の親子兄弟要素へアクセスするには、次のようなElement
オブジェクトのプロパティからアクセスすることが可能です。
const elment = document.querySelector('selector'); // すべての子要素を参照 const children = element.children; // 最初の子要素を参照 const firstChild = element.firstElementChild; // 最後の子要素を参照 const lastChild = element.lastElementChild; // 親要素を参照 const parent = element.parentElement; // 一つ前の兄弟要素を参照 const prev = element.previousElementSibling; // 次の兄弟要素を参照 const next = element.nextElementSibling;
上記の各プロパティの型は次の通りです。
Element.children | HTMLCollection |
---|---|
Element.firstElementChild | Element |
Element.lastElementChild | Element |
Element.parentElement | Element |
Element.nextElementSibling | Element |
Element.previousElementSibling | Element |
NodeとElement
Node
はElement
よりも低レベルのDOMインターフェースで、Element
はNode
を継承しています。DOM操作用APIにはElement
だけでなく、Node
を扱う関数も数多く用意されています。
例えば、Element.parentElement
は親要素を返しますが、Element.parentNode
は親ノードを返す、といった具合にエレメント操作用関数と対になっているものも数多くあります。
ただ、実際のコーディングにおいては、Node
よりもElement
を操作することが殆どではないかと思いますので、本記事ではNode
操作の詳細については割愛しています。
基本はquerySelector, querySelectorAll
HTMLCollection
とNodeList
とでは性質や扱い方に若干の違いがあることは先に述べた通りです。DOM操作の出発点として、まず既存のDOMを取得するケースが多いかと思いますが、その際document.getElementsByClassName
やdocument.querySelectorAll
等が混在していると、取得したオブジェクトもHTMLCollection
やNodeList
が混在してしまいます。
個人的な見解としては、DOM取得の際は返されるオブジェクトがHTMLCollection
なのか、NodeList
なのかを意識つつ、極力どちらかに寄せていった方がコードに一貫性があるので良いと考えています。
さらに言うと、document.querySelector
/document.querySelectorAll
の方がCSSセレクタでDOMを抽出できるので、こちらの方が便利かなと思います。ただし、document.querySelectorAll
はあらゆる情報を即座に静的リストに集めるためパフォーマンスの低下を引き起こすとのことですので、大量のDOMを一度に取得するような場合は限定的にHTMLCollection
を扱うようにした方が良いかもしれません。
要素の作成
要素(タグ)の新規作成にはdocument.createElement
関数を使用します。引数にはタグ名を指定します。
const elm = document.createElement('p');
上のコードはpタグを新規作成した例です。ここでは要素を作成しただけで、まだDOMツリーに追加していませんので、このままでは画面に表示されることはありません。作成した要素をDOMツリーに追加するには、次のDOM追加用関数を使用する必要があります。また、上の例では属性も中身もないただの空のpタグを作成しただけなので、このタグに属性や中身(テキストやその他の要素など)を追加するにはこの後説明する各種関数を利用して操作していく必要があります。
要素の追加
ある要素に別の要素を追加する場合は、Element.appendChild
、Element.insertBefore
メソッドが使えます。
appendChild
は指定した要素の子要素の一番最後に新たな要素を加えます。insertBefore
は指定した要素の子要素のうち、特定の要素の直前に新たな要素を加えます。
<ul> <li>その1</li> <li>その2</li> </ul>
const ul = document.querySelector('ul'); const li3 = document.createElement('li'); const li4 = document.createElement('li'); const li5 = document.createElement('li'); li3.textContent = 'その3'; li4.textContent = 'その4'; li5.textContent = 'その5'; // ulの子要素の末尾に追加 ul.appendChild(li3); // 「その1」と「その2」の間に追加 ul.insertBefore(li4, ul.children[1]); // insertBeforeの第二引数をnullにすると子要素の末尾に追加される ul.insertBefore(li5, null);
appendChild
/insertBefore
という名前から連想すると、prependChildやinsertAfterのといったメソッドもありそうな気がしますが、Vanilla JSではそのようなメソッドは存在しません(jQueryではprepend
やinsertAfter
といったメソッドがあります)。
「子要素の先頭に挿入」「子要素の中の特定の要素の後に挿入」といったことをするにはどちらもinsertBefore
を使って実装することができます。
const ul = document.querySelector('ul'); const li6 = document.createElement('li'); const li7 = document.createElement('li'); li6.textContent = 'その6'; li7.textContent = 'その7'; // ulの子要素の先頭に追加 ul.insertBefore(li6, ul.firstElementChild); // ulの子要素の中の特定の要素の「後」に挿入 // (nextElementSibling = 対象要素の「次」の兄弟要素) ul.insertBefore(li7, ul.children[0].nextElementSibling);
要素の変更
コンテンツ(要素の中身)の操作
要素のコンテンツを修正するには、element.textContent
やelement.innerHTML
プロパティを設定します。
textContent
プロパティは要素内のテキストを、innerHTML
プロパティは要素内のHTMLをそれぞれ取得・設定することができます。
textContent
プロパティはあくまでもテキストなので、例えばHTMLタグを含む文字列を設定したとしても、そのタグはテキストとして表示されるだけなので、新たなDOMツリーは作成されません。
textContent
、innerHTML
のどちらも既存のコンテンツを置き換えてしまうので、注意が必要です。
また、textContent
と似たinnerText
というプロパティがありますが、これはIEの独自インターフェースであり標準仕様ではありませんので使用を避けた方が良さそうです。(IE9以降、textContentプロパティが使用可能になったので。)
<div id="container"> <p class="text">foo</p> </div>
const container = document.getElementById('container'); container.textContent = 'bar';
<div id="container"> bar </div>
const container = document.getElementById('container'); container.innerHTML = '<span class="text">bar</span>';
<div id="container"> <span class="text">bar</span> </div>
属性の操作
ある要素に対し属性をセットする場合はsetAttribute
メソッドを、設定されている属性を取得する場合はgetAttribute
メソッドを使用します。
setAttribute
メソッドでは既に同じ属性が設定されている場合は上書きされます。また、getAttribute
は存在しない属性名を指定した場合はnullを返します。
属性の削除にはremoveAttribute
メソッドを使用します。removeAttribute
メソッドの引数には属性名を渡しますが、未設定の属性名を渡したとしても例外は発生しません。
属性の有無を判定するにはhasAttribute
メソッドを使用します。
すべての属性の取得にはattributes
プロパティが利用できます。attributes
もHTMLCollection
やNodeList
同様、配列に似たオブジェクトですが、厳密には配列ではなくNamedNodeMap
という属性の集合を表すオブジェクトです。
// 属性の取得 element.getAttribute('id'); // 属性のセット element.setAttribute('id', 'test'); // 属性の element.removeAttribute('id'); // 属性を持っているかどうか element.hasAttribute('id'); // すべての属性のコレクションを取得 const atts = element.attributes;
クラスの操作
classの取得/設定を行う最も単純な方法はElement.className
プロパティを利用する方法です。className
プロパティは対象要素に指定されたclass属性値(文字列)をダイレクトに取得/設定することができます。
<div class="foo bar"></div>
const elm = document.querySelector('.foo'); console.log(elm.className); // foo bar elm.className = 'baz'; console.log(elm.className); // baz
className
プロパティは単純にclass属性値の文字列を取得/設定するだけなので、classの追加や削除、または特定のクラスのオン/オフを切り替えたいようなケースでは少し扱いづらいです。
そのような場合は、classList
プロパティの各種関数を利用するのが良いでしょう。
const elm = document.querySelector('.foo'); console.log(elm.className); // foo // classの追加 elm.classList.add('bar'); console.log(elm.className); // foo bar // (カンマ区切りで複数一括追加も可能) elm.classList.add('hoge', 'fuga'); console.log(elm.className); // foo bar hoge fuga // classの削除 elm.classList.remove('bar'); console.log(elm.className); // foo hoge fuga // (カンマ区切りで複数一括削除も可能) elm.classList.remove('hoge', 'fuga'); console.log(elm.className); // foo // classのON/Off elm.classList.toggle('bar'); console.log(elm.className); // foo bar elm.classList.toggle('bar'); console.log(elm.className); // foo // クラスの有無判定 console.log(elm.classList.contains('bar')); // false
要素の削除
特定の要素を削除するにはElement.removeChild
メソッドが利用できます。removeChild
の引数には削除対象となるelement
オブジェクトを渡します。removeChild
関数の戻り値は削除されたelementオブジェクトです。(つまり、引数で渡したelement
オブジェクトが返されます)。
削除されたelementオブジェクトはDOMツリーからは削除されますが、スクリプト内での参照が生きている限りアクセスすることは可能です。
<ul id="items"> <li id="foo">foo</li> <li>bar</li> </ul> <p id="text">baz</p>
const items = document.getElementById('items'); const foo = document.getElementById('foo'); const text = document.getElementById('text'); console.log(foo); //特定の子要素削除 const removed = items.removeChild(foo); console.log(removed === foo); // true //自分を削除する場合は items.parentElement.removeChild(items); //子要素を全て削除(高速) while (items.firstChild) { items.removeChild(items.firstChild); } //子要素を全て削除(低速) items.textContent = null; //子要素ではない要素を指定するとエラーになる items.removeChild(text); //Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this
要素の移動
要素を移動するには、element.appendChild
、element.insertBefore
関数が利用できます。
どちらの関数も、「要素の追加」の項で出てきた関数ですが、既にDOMツリーに組み込まれている既存の要素を引数として渡すことで、その要素をツリーの別の場所に移動させることができます。
<ul id="list1"> <li>foo</li> <li>bar</li> <li>baz</li> </ul> <ul id="list2"> <li>hoge</li> <li>fuga</li> <li>piyo</li> </ul>
const list1 = document.getElementById('list1'); const list2 = document.getElementById('list2'); const hoge = list2.children[0]; const fuga = list2.children[1]; list1.appendChild(hoge); list1.insertBefore(fuga, items1.firstElementChild);
<ul id="list1"> <li>fuga</li> <li>foo</li> <li>bar</li> <li>baz</li> <li>hoge</li> </ul> <ul id="list2"> <li>piyo</li> </ul>
対応ブラウザについて
jQueryを使わない場合、ブラウザの対応状況を気にする必要があります。すべてを網羅してちゃんと調べたわけではありませんが、本記事で紹介した各種DOM操作用のメソッドやプロパティについては現時点での最新ブラウザでは問題無く使えるかと思います。
本記事では触れていないトピックのブラウザ対応状況についてはCan I use… Support tables for HTML5, CSS3, etc等を使ってご確認ください。
まとめ
今回はjQueryを使わないDOM操作の全体感を掴むためのリサーチだったので、ここで挙げたDOM操作用のインターフェースはあくまで一部に過ぎませんが、現場での使用が想定されるものは概ねこんなところではないかと思います。さらに詳細なAPIの仕様についてはMDNとかを参照してください。
以前は一口にDOM操作といっても、ブラウザごとに実装にばらつきがあったり、API自身の実装も今ほど充実していませんでした。そういった未熟な部分をjQueryが補完してくれていました。
現在はブラウザ毎の実装の違いもほとんどなく、標準のDOM操作インターフェースも充実しているので、jQueryを使用しなくても納品物としてそのまま使用できるレベルにまで達してきたのではないかと思います。(IE11もサポートする場合はちゃんとチェックしてから使用した方が無難な気もしますが)
jQueryが最も重宝されていた理由として、クロスブラウザ対応や簡単なDOM操作といった点が挙げられるかと思いますが、ネイティブでのDOM操作が現場での使用に耐えられるようになってきた昨今、jQueryを利用するメリットは薄くなってきたのかもしれません。
しかし個人的な意見ですが、ことDOM操作という観点で言えばやはりjQueryを使った方がシンプルで直感的にコーディングできるな、とも思いました。jQueryを使用する場合、ライブラリファイルのロードにかかるオーバーヘッドや、DOM操作におけるパフォーマンスが気になるかもしれませんが、調べた限りでは両者に体感できるほどの違いは無さそうです。どちらを使うべきかというのは無く、プログラマーの好みであったり、その他もろもろの制約であったりと、ケースバイケースでしょう。
jQuery自体は今後も生き残っていくと思いますが、ブラウザ側の実装が充実していくにつれ、jQueryの恩恵は確かに薄くなっていき、jQuery離れも今以上に進んでくるはずです。そういう意味ではネイティブでの書き方を覚えることでjQueryへの依存を減らしていくことは良いことだと考えます。
参考リンク
- jQueryによるDOM操作をまとめてみた – Rails Weboo
- ライブラリを使わない素のJavaScriptでDOM操作 – Qiita
- もうjQueryには頼らない!素のJavaScriptでDOMを操作するための基礎知識 – WPJ
- DOMノードの取得方法メモ書き – Qiita
- NodeListとHTMLCollectionも別物なので気を付けよう。(DOMおれおれAdvent Calendar 2015 – 13日目) | Ginpen.com
- 配列ライクなオブジェクトをforEachするときのイディオム – ぷちてく – Petittech
- 【JavaScript】classListとclassNameを使用したclass属性の操作について – TASK NOTES
- そのコード、本当にjQueryが必要ですか?ネイティブ関数の代替Tips集 | ゆっくりと…
- JQuery speed vs javascript speed – Stack Overflo
- もうjQueryには頼らない!素のJavaScriptでDOMを操作するための基礎知識 – WPJ