【STEP.30】
DOMをマスターしよう!(Part.01)
~要素・属性・プロパティの操作編~

DOM(Document Object Model)とは

クライアントサイドJavaScriptプログラミングでは、「エンドユーザーや外部サービスからなんらかの入力を受け取って、これを処理した結果をページに反映させる」という流れが一般的です。

「ページに反映させる」とは、つまり、HTMLをJavaScriptで編集するということです。

この際、もちろん文字列として編集することも可能です。

しかし、一般的に、複雑な文字列の編集はコードも読みにくくなり、バグのもとともなります。

<div>要素やアンカータグといったかたまりで、オブジェクトとして操作できた方が便利です。

そこで登場するのがDOM(Document Object Model)です。

DOMは、マークアップ言語(HTML、XMLなど)で書かれたドキュメントにアクセスするための標準的な仕組みで、JavaScriptに限らず、現在よく利用されている言語のほとんどがサポートしています。

DOMは、Web技術の標準化団体であるW3C(World Wide Web Consortium)で標準化が進められ、現在は4つのレベル(Level1~4)があります。

また、時として「DOM Level 0」「DOM 0」という言葉が登場することもありますが、これはDOM Level 1が策定される前の(標準化されていない)ブラウザーオブジェクトのことです。

標準仕様として「DOM Level 0」が存在するわけではありません。

文書ツリーとノード

DOMは、ドキュメントを文書ツリー(ドキュメントツリー)として扱います。

たとえば、次のようなコードであれば、DOMの世界では図のようなツリー構造と解釈されることになります。

<!DOCTYPE html>  
<html>
<head>
  <meta charset="UTF-8">
  <title>ユタスク【Utask】</title>
</head>
<body>
  <p id="greet">これが<strong>文書ツリー</strong>です。</p>
</body>
</html>

dom.htmlのツリー構造

DOMでは、Document Object Modelという名前の通り、文書に含まれる要素や属性、テキストをそれぞれオブジェクトと見なし、「オブジェクトの集合(階層関係)」と考えるわけです。

ちなみに、文書を構成する要素や属性、テキストといったオブジェクトのことをノードと呼び、オブジェクトの種類に応じて要素ノード属性ノードテキストノードなどと呼びます。

DOMは、これらノードを抽出/追加/置換/削除するための汎用的な手段を提供するAPI(Application Programming Interface)といえます。

それぞれのノードは、ツリー上での上下関係によって、以下のように呼ばれることもあります。

これらの呼称は、ルートノードを除いて相対的なものです。

あるノードに対して子ノードだったノードが、別のノードに対して親ノードになることもあります。

【ノードの種類】
ノード概要
ルートノードツリーの最上位に位置するノード。最上位ノードと呼ぶ場合もあります。
親ノード/子ノード上下関係にあるノード。直接つながっているノードで、ルートノードに近いノードを親ノード、遠いノードを子ノードと呼びます(上下関係にあるが、直接の親子でないものを先祖ノードと子孫ノードと呼ぶ場合もあります)。
兄弟ノード同じ親ノードを持つノード同士。先に書かれているものを兄ノード、後に書かれているものを弟ノードとして区別する場合もあります。

クライアントサイドJavaScript(DOM)で開発を進めていくうえで、最初に押さえておきたい事項が以下の3つです。

  1. 要素/属性/プロパティの取得
  2. 文書ツリー間の行き来(ノードウォーキング)
  3. イベントドリブンモデル(addEventListenerメソッド)

このページでは、(1)の要素/属性/プロパティの取得について詳しく解説しています。

(2)の文書ツリー間の行き来(ノードウォーキング)については参考ページをご覧ください。
(3)のイベントドリブンモデル(addEventLintenerメソッド)については参考ページをご覧ください。

id値をキーに要素を取得する【getElementById】

getElementByIdメソッドを利用します。

getElementByIdメソッド

document.getElementById(id)
id    取得したい要素のid値

id値はページで一意なはずなので、getElementByIdメソッドの戻り値も単一の要素(Elementオブジェクト)です。

たとえば以下は、<span id=”desc”>要素のテキストを取得しています。

<span id="desc">Hello、JavaScript!</span>
let desc = document.getElementById('desc');
	
console.log(desc.textContent);

// 結果:Hello、JavaScript!
MEMO
id値が重複した場合
ページ内に同じid値を持つ要素が存在した場合も、getElementByIdメソッドは最初に見つかった要素を返します。

ただし、この挙動は、ブラウザーの種類/バージョンによって変化する可能性があります。

本来、ページ内のid値は一意であるべきです。

タグ名をキーに要素を取得する【getElementsByTagName】

getElementsByTagNameメソッドを利用します。

getElementsByTagNameメソッド

document.getElementsByTagName(name)
name    取得したい要素(タグ)の名前

タグ名での検索では複数の要素がマッチする可能性があるので、getElementsByTagNameメソッドの戻り値も要素の集合(HTMLCollectionオブジェクト)です。

html,head,title,body,id,#text,結果は複数の可能性がある,getElementsByTagName('p'),結果は必ず1つ,getElementById('z')

引数nameに「*」を指定することで、すべての要素を取得することもできます。

たとえば以下は、ページ内のすべての<h2>要素を取得し、そのテキストを列挙する例です。

<h2>春</h2>
<p>陽春の春</p>
<h2>夏</h2>
<p>盛夏の夏</p>
<h2>秋</h2>
<p>初秋の秋</p>
<h2>冬</h2>
<p>大寒の冬</p>
let elems = document.getElementsByTagName('h2');

for (let i =0; i < elems.length; i++) {
  console.log(elems.item(i).textContent);
}
// 結果:春、夏、秋、冬

JavaScriptのソースコード3行目では、インデックス数を0~length-1(ここでは3)まで変化させることで、リストから要素ノード(h2タグ)を1つずつ取り出しています。

getElementsByTagName('h1'),合致したタグを検索/取得,HTMLCollectionオブジェクト,length

要素の集合(HTMLCollection)で利用できるメンバーは、次に掲げる表の通りです。

【HTMLCollectionオブジェクトのメンバー】
メンバー概要
lengthリスト内の要素数
item(index)index番目(先頭は0)の要素を取得
namedItem(name)id、またはname属性にマッチする要素を取得

itemnamedItemメソッドは、ブラケット構文でも置き換え可能です。

そのため、「console.log(elems.item(i).textContent);」(JavaScriptのソースコード4行目)は、以下のように書いても同じ意味です。

console.log(elems.item[i].textContent);

name属性をキーに要素を取得する【getElementsByName】

getElementsByNameメソッドを利用します。

getElementsByName

document.getElementsByName(name)
name    取得したい要素のname属性

主に<input>要素を取得するために利用します。

ただし、単一の要素があればgetElementByIdメソッドを利用したほうが便利なので、name要素が同じで、複数の要素があることが前提となるラジオボタン/チェックボタンでの利用に限られます。

たとえば、以下はラジオボタンを取得し、その値を列挙する例です。

<form>
  お使いのOSは?:
  <label>
    <input type="radio" name="os" value="window" />Windows
  </label>
  <label>
    <input type="radio" name="os" value="mac" />Mac OS
  </label>
  <label>
    <input type="radio" name="os" value="unix" />Unix
  </label>
</form>
let elems = document.getElementsByName('os');
	
for (let i = 0; i < elems.length; i++) {
  console.log(elems.item(i).value);
  // 結果:windows,mac,unix
}

getElementsByNameメソッドの戻り値はNodeList(ノードの集合)です。

機能的には、HTMLCollectionとほぼ同じととらえてかまいません(NodeListでは、namedItemメソッドを利用できないくらいです)。

class属性をキーに要素を取得する【getElementsByClassName】

getElementsByClassNameメソッドを利用します。

getElementsByClassNameメソッド

document.getElementsByClassName(clazz)
clazz    取得したい要素のclass属性

特定の役割(意味)を持った要素にあらかじめ共通のクラス名(class属性)を付与しておくことで、まとめて目的の要素(群)を取得できます。

たとえば以下は、ページからclass属性が「"keywd"」である要素だけを取り出し、その値(テキスト)を列挙する例です。

<p>人間失格</p>
<p>「恥の多い生涯を送って来ました」。<br/>
そんな身もふたもない告白から男の手記は始まる。<br/>
男は自分を偽り、ひとを欺き、取り返しようのない過ちを犯し、<span class="keywd">「失格」</span>の判定を自らにくだす。<br/>
でも、男が不在になると、彼を懐かしんで、ある女性は語るのだ。<br/>
「とても素直で、よく気がきいて(中略)神様みたいないい子でした」と。<br/>
ひとがひととして、ひとと生きる意味を問う、<span class="keywd">太宰治</span>、捨て身の問題作。
</p>
let elems = document.getElementsByClassName('keywd');
	
for (let i = 0; i < elems.length; i++) {
  console.log(elems.item(i).textContent);
}
「失格」
太宰治

引数clazzには「c1 c2」のようにスペース区切りで複数のクラスを指定することもできます。

その場合、class属性にc1、c2の双方がある要素を取得できます。

セレクター式で要素を検索する【querySelector/querySelectorAll】

querySelectorquerySelectorAllメソッドを利用します。

querySelector/querySelectorAllメソッド

document.querySelector(selectors)
document.querySelectorAll(selectors)
selectors    セレクター式

querySelectorメソッドは最初にマッチした要素を1つだけ、querySelectorAllメソッドはマッチしたすべての要素を、それぞれ返します。

最初から取得すべき要素が1つとわかっている場合、もしくは、要素群の最初の1つだけを取り出したい場合にはquerySelectorメソッドを、さもなくばquerySelectorAllメソッドを利用します。

たとえば以下は、「id="menu"」である要素配下から、すべての<a>要素を取り出し、href属性の値を列挙する例です。

<ul id="menu">
  <li><a href="https://jquery.com/">トップページ</a>
  <li><a href="https://jquery.com/download/">ダウンロード</a>
  <li><a href="https://blog.jquery.com/">ブログ</a>
  <li><a href="https://api.jquery.com/">API</a>
  <li><a href="https://learn.jquery.com/">ラーニングセンター</a>
</ul>
let elems = document.querySelectorAll('#menu a');
	
for (let i = 0; i < elems.length; i++) {
  console.log(elems.item(i).href);
}
https://jquery.com/
https://jquery.com/download/
https://blog.jquery.com/
https://api.jquery.com/
https://learn.jquery.com/

セレクター式一覧

引数selectorsで指定できるセレクター式は、基本的にCSSのそれに準じます。

次に掲げる表に、主なものをまとめます。

【引数selectorで利用できる構文】
分類構文概要
基本*すべての要素を取得*
#id指定したID値の要素を取得#myself
.class指定したクラス(class属性)の要素を取得.books
element指定したタグ名の要素を取得h2
seloctor1,selector2,selectorN複数のセレクターのいずれかにマッチする要素をまとめて取得#myself,h2
階層ancestor descendant要素ancestorを先祖とする子孫要素descendantを取得.main span
parent > child要素parentを親とする子要素childを取得#myself > div
prev + next要素prevの次の要素nextを取得#myself + div
prev ~ siblings要素prev以降の兄弟要素siblingsを取得#myself ~ div
フィルタ/基本:rootドキュメントのルート要素を取得:root
:not(exp)セレクターexpにマッチしない要素を取得p:not(.main)
:lang(lang)指定した言語要素をすべて取得:lang(ja)
:empty子要素を持たない要素を取得div:empty
フィルタ/属性[attr]指定した属性を持つ要素を取得div[class]
[attr = value]属性が値valueに等しい要素を取得div[class = "main"]
[attr ^= value]属性がvalueで始まる値を持つ要素を取得[id ^= "win"]
[attr $= value]属性がvalueで終わる値を持つ要素を取得[id $= "er"]
[attr *= value]属性がvalueを含む値を持つ要素を取得li[id *= "test"]
フィルタ/子要素:nth-child(index | even | odd)引数(インデックス/偶数/奇数)番目の要素を取得li:nth-child(2)
:nth-last-child(index | even | odd)末尾から引数(インデックス/偶数/奇数)番目の要素を取得li:nth-last-child(even)
:nth-of-type(index | even | odd)指定した兄弟要素の中で引数(インデックス/偶数/奇数)番目の要素を取得li:nth-of-type(even)
:nth-last-of-type(index | even | odd)指定した兄弟要素の中で末尾から引数(インデックス/偶数/奇数)番目の要素を取得li:nth-last-of-type(even)
フィルタ/子要素:first-child最初の子要素を取得div:first-child
:last-child最後の子要素を取得div:last-child
:first-of-type指定した兄弟要素の中で最初の要素を取得div:first-of-type
:last-of-type指定した兄弟要素の中で最後の要素を取得div:last-of-type
:only-child子要素を1つだけ持つ要素を取得:only-child
:only-of-type指定した要素名で他に兄弟要素を持たない要素すべてを取得p:only-of-type
フィルタ/フォム状態:enabled有効な状態にある要素をすべて取得:enabeld
:disabled無効な状態にある要素をすべて取得:disabled
:checkedチェック状態にある要素をすべて取得:checked
:focusフォーカスが当たっている要素を取得:focus

このように、querySelectorquerySelectorAllは複雑な検索条件も表現できる、高機能なメソッドですが、そのためにgetElementXxxxxx系のメソッドに比べると、低速です。

特定のid値、classname属性で要素を特定できる場合には、まずはgetElementXxxxxxメソッドを利用します。

特にgetElementByIdメソッドは高速なので、それで賄える状況では、できるだけid値での検索を優先します。

ここまで解説してきたgetElementXxxxxquerySelectorquerySelectorAllメソッドは、documentオブジェクトではなく特定の要素(Elementオブジェクト)から呼び出すこともできます。

その場合は、その要素の配下からのみ目的の要素を検索します。

let list = ducument.getElementById('list');
// 文章全体を検索
let opts = list.getElementsByTagName('option');
// 要素配下だけを取得

目的の要素が特定の親要素の配下にあることがわかっており、かつ、親要素が既に取得できている場合には、それを基点にした方が検索は効率的です。

要素の属性を設定する【setAttribute】

setAttributeメソッドを利用します。

setAttributeメソッド

element.setAttribute(name, value)
name    属性名
value   属性値

たとえば以下は、「rel = "external"」であるアンカータグに対して、title属性を設定する例です。

<ul id="menu">
  <li><a href="https://yutamanshop.com/" target="_self">トップページ</a>
  <li><a href="https://jquery.com/" target="_blank" rel="external">jQueryトップページ</a>
  <li><a href="https://api.jquery.com/" target="_blank" rel="external">jQueryAPI</a>
  <li><a href="https://learn.jquery.com/" target="_blank" rel="external">jQueryラーニングセンター</a>
</ul>
let exs = document.querySelectorAll('a[rel = "external"]');
	
for (let i = 0; i < exs.length; i++) {
  exs[i].setAttribute('title','外部サイトに移動します。');
}
setAttributeメソッド

要素の属性を取得する【getAttribute/attributesプロパティ】

getAttributeattributesメソッドを利用します。

指定された属性を取得する【getAttribute】

指定された属性を取得するのは、getAttributeメソッドの役割です。

getAttributeメソッド

element.getAttribute(name)
name    属性名

たとえば以下は、「class="outer"」であるアンカータグのhref属性を取得します。

<ul id="menu">
  <li><a href="https://yutamanshop.com/">トップページ</a>
  <li><a href="https://jquery.com/" class="outer">jQueryトップページ</a>
  <li><a href="https://api.jquery.com/" class="outer">jQueryAPI</a>
  <li><a href="https://learn.jquery.com/" class="outer">jQueryラーニングセンター</a>
</ul>
let elems = document.getElementsByClassName('outer');
	
for (let i = 0; i < elems.length; i++) {
  console.log(elems.item(i).getAttribute('href'));
}
https://jquery.com/
https://api.jquery.com/
https://learn.jquery.com/

すべての属性を取得する【attributesプロパティ】

現在の要素に含まれるすべての属性を取得するには、attibutesプロパティを利用します。

以下は、<img id="main">要素からすべての属性を列挙する例です

<img id="main" src="https://bit.ly/3bsvSWU" width="680" height="390" border="0" alt="halloween" />
let main = document.getElementById('main');

let attrs = main.attributes;

for (let i = 0; i < attrs.length; i++) {
  let attr = attrs.item(i);
  console.log(attr.name + ':' + attr.value);
}
id:main
src:https://bit.ly/3bsvSWU
width:680
height:390
border:0
alt:halloween

attributesプロパティの戻り値は、属性の集合(NamedNodeMapオブジェクト)です。

NamedNodeMapオブジェクトで利用できるメンバーは、次に掲げる表の通りです。

【NamedNodeMapオブジェクトの主なメンバー】
メンバー概要
length集合の要素数
getNamedItem(name)属性名がnameの要素を取得
setNamedItem(node)属性ノードnodeを設定
removeNamedItem(name)属性名がnameの要素を削除
item(index)index番目の要素を取得

HTMLCollectionNodeListにも似ていますが、個々の属性に対して設定/削除もできる点が異なります。

要素の属性を削除する【removeAttribute】

removeAttributeメソッドを利用します。

removeAttributeメソッド

document.removeAttribute(name)
name    属性名

たとえば以下は、すべてのアンカータグからtarget属性を削除する例です。

<ul id="menu">
  <li><a href="https://yutamanshop.com/" target="_self">トップページ</a>
  <li><a href="https://jquery.com/" target="_blank">jQueryトップページ</a>
  <li><a href="https://api.jquery.com/" target="_blank">jQueryAPI</a>
  <li><a href="https://learn.jquery.com/" target="_blank">jQueryラーニングセンター</a>
</ul>
let elems = document.getElementsByTagName('a');

for (let i = 0; i < elems.length; i++) {
  elems.item(i).removeAttribute('target');
}

removeAttributeメソッド

要素に削除すべき属性がなくても、removeAttributeメソッドは例外を発生させません。

要素に指定の属性が存在するかどうかを判定する【hasAttribute】

hasAttributeメソッドを利用します。

hasAttributeメソッド

element.hasAttribute(name)
name    属性名

たとえば以下は、<img>要素にsrc属性が設定されていない場合、src属性を設定する例です。

<img src="https://bit.ly/3dM2mNp" />
<img />
<img src="https://bit.ly/3bvul20" />
let elems = document.getElementsByTagName('img');

for (let i = 0; i < elems.length; i++) {
  if (!elems.item(i).hasAttribute('src')) {
    elems.item(i).setAttribute('src', 'https://bit.ly/2y0GTkz');
  }
}

要素のプロパティを取得/設定する

要素は、ほとんどの属性について同名のプロパティを提供しています。

多くの状況で、プロパティを取得すると属性の値が取得できたり、プロパティ設定によって属性に反映されたりするので、一見して同じもののように見えますが、厳密には両者は異なる概念です。

特に、以下の状況では、明確に双方を使い分ける必要があります。

属性とプロパティの違い

フォーム要素への入力値

属性は要素の初期値を表すのに対して、プロパティは現在値を表します。

つまり、getAttributeメソッドでユーザーからの入力値を受け取ることはできません。

<form>
  <div>
    <label for="name">氏名</label>
    <input id="name" type="text" name="name" value="横浜流星" />	
  </div>
  <input id="btn" type="button" value="送信" />
</form>
let name = document.getElementById('name');

document.getElementById('btn').addEventListener('click', function (e) {
  console.log(name.value);
  console.log(name.getAttribute('value'));
}, false);

valueプロパティ/getAttributeプロパティ

ブール属性

selectedcheckeddisabledmultipleなど、値がいらない(=属性名を指定するだけで意味がある)属性のことを論理属性ブール属性といいます。

これらの値にアクセスした場合、getAttributeメソッドはマークアップ上の属性値を返しますが、プロパティ構文ではtruefalseを返します。

コード上で扱う際には属性の表記によって値が変化するのは不便ですし、truefalse値のほうが扱いやすいので、プロパティ構文を利用すべきです。

<form>
  <div>
    <label for="name">氏名</label>
    <input id="name" type="text" name="name" value="北村匠海" disabled/>	
  </div>
  <div>
    <label for="os">使用OS</label>
    <select id="os" multiple="multiple">
      <option id="win" value="windows" selected="">Windows
      <option id="mac" value="mac">Mac OS
      <option id="lin" value="linux" selected="">Linux
    </select>
  </div>
</form>
let name = document.getElementById('name');
let os = document.getElementById('os');
let win = document.getElementById('win');

	
console.log(name.disabled);
// 結果:true
console.log(name.getAttribute('disabled'));
// 結果:(空白)
console.log(os.multiple);
// 結果:true
console.log(os.getAttribute('multiple'));
// 結果:multiple
console.log(win.selected);
// 結果:true
console.log(win.getAttribute('selected'));
// 結果:(空白)

そもそもプロパティにしかない(属性が存在しない)情報

次に掲げる表のようなプロパティは、属性に対応するものがありません。

これらは当然プロパティ構文を利用するしかありません。

【要素オブジェクトからアクセスできる主なプロパティ】
プロパティ概要
nodeNameノードの名前
tagNameタグ名
nodeTypeノードの種類

逆に、<td>要素のcolspanrowspanのように、属性には存在するが、対応するプロパティが存在しないものもあります。

これらはgetAttributeメソッドでアクセスしなければなりません。

要素配下にテキストを設定する【textContentプロパティ/innertHTMLプロパティ】

textContentinnerHTMLプロパティを利用します。

両者の違いは、タグ文字列を認識するかどうかです。

もっと具体的には、textContentプロパティでは指定されたタグ文字列をそのまま埋め込みますが、innerHTMLプロパティではHTMLとして解釈します。

<p id="ih"></p>
<p id="tc"></p>
let ih = document.getElementById('ih');

let tc = document.getElementById('tc');

ih.innerHTML = '<img src="https://bit.ly/3dM2mNp" />';

tc.textContent = '<img src="https://bit.ly/3bvul20" />';

textContenteプロパティ/innnerHTMLプロパティ

一般的には、意図してHTML文字列を埋め込むのでなければ、まずは、

textContentプロパティを利用します。

特に、ユーザーからの入力値や外部サービスから得た値を、そのままinnerHTMLプロパティに渡してはいけません。

たとえば入力値が「<div onclick="...">...</div>」のような文字列を含んでいた場合には、勝手に任意のコードが実行されてしまう原因になります。

MEMO
<script>要素は実行されない
ちなみに、innerHTML経由で挿入された<script>要素は実行されません。

たとえば、「<script>alert('NG!!');</script>」のような文字列をinnerHTMLプロパティに渡しても実行はされません。

これによって、innerHTMLでも最低限、脆弱性の混入を防止しているわけです。

もっとも、onclick属性などの属性を使えば、<script>以外でもコードを混入させることは可能です。

あくまでも一時的な対策と割り切り、可能な限り、textContentプロパティを優先して利用します。

textContent/innerHTMLプロパティは取得する対象が異なる

textContentinnerHTMLプロパティでは、取得する対象も異なります。

innerHTMLプロパティは、対象となる要素の配下をタグ込みでまとめて返しますが、textContentプロパティは、子要素それぞれからテキストだけを取り出して連結したものを返します。

<div id="lang">
  <p>JavaScript</p><p>jQuery</p>
</div>
let lang = document.getElementById('lang');

console.log(lang.innerHTML);
console.log(lang.textContent);
<p>JavaScript</p><p>jQuery</p> innerHTMLプロパティ
JavaScriptjQuery textContentプロパティ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です