DOM の構造
DOM は XML または HTML 文書をツリー構造として表します。このページでは、DOM ツリーの基本構造と、その操作に使用される様々なプロパティおよびメソッドについて紹介します。
まず、ツリーにまつわるいくつかの概念を紹介する必要があります。ツリーとは、ノードで構成されるデータ構造です。それぞれのノードはデータを保持します。ノードは階層的に組織化されており、それぞれのノードは単一の親ノード(親を持たないルートノードを除く)と、0 個以上の子ノードの順序付きリストを持ちます。これで以下の定義が可能です。
- 親を持たないノードは、ツリーのルートと呼ばれます。
- 子ノードを持たないノードは葉ノードと呼ばれます。
- 同じ親を持つノードは兄弟ノードと呼ばれます。兄弟ノードは親の同一の子ノードリストに属するため、明確な順序が定義されています。
- ノード A から親リンクを繰り返し追跡することでノード B へ到達可能な場合、A は B の子孫であり、B は A の祖先であるといえます。
- ツリー内ノードは、まずノード自体を列挙し、次にそれぞれの子ノードを順序通りに再帰的に列挙する(先順序、深さ優先探索)というツリー順序でリスト化されます。
そして、以下のようなツリーに関するいくつかの重要なプロパティがあります。
- 各ノードは単一のルートノードに関連付けられています。
- ノード A がノード B の親である場合、ノード B はノード A の子であるといえます。
- 循環は許容されません。いかなるノードも、自分自身の祖先または子孫であってはなりません。
Node インターフェイスとそのサブクラス
DOM 内のすべてのノードは、Node インターフェイスを実装するオブジェクトによって表されます。Node インターフェイスは、これまでに定義された概念の多くを体現しています。
parentNodeプロパティあ h 親ノードを返します。ノードに親がない場合はnullを返します。childNodesプロパティは子ノード群のNodeListを返します。firstChildプロパティとlastChildプロパティはそれぞれこのリストの先頭と末尾の要素を返します。子ノードがない場合はnullを返します。getRootNode()メソッドは、親のリンクをたどって、このノードを含むツリーのルートを返します。hasChildNodes()メソッドは、子ノードがある場合、つまり葉ノードではない場合にtrueを返します。previousSiblingプロパティとnextSiblingプロパティは、それぞれ直前と直後の兄弟ノードを返します。そのような兄弟がない場合はnullを返します。contains()メソッドは、指定されたノードがこのノードの子孫である場合にtrueを返します。compareDocumentPosition()メソッドは、 2 つのノードをツリー順で比較します。このメソッドはノードの比較の節で詳しく解説します。
純粋な Node オブジェクトを直接扱うことはほとんどありません。代わりに、DOM 内のすべてのオブジェクトは Node を継承したインターフェイスのいずれかを実装しており、これらは文書内の追加的な意味論を表します。ノードの型は、そのノードが保持するデータや有効な子ノードの型を制限します。次の HTML 文書が DOM でどのように表現されるかを考えてみましょう:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</body>
</html>
これは次のような DOM ツリーを生成します。
この DOM ツリーのルートは Document ノードであり、文書全体を表します。このノードは変数 document としてグローバルに公開されています。このノードには 2 つの重要な子ノードがあります。
DocumentTypeノードはオプションで、doctype 宣言を表します。この場合は 1 つ存在します。このノードはDocumentノードのdoctypeプロパティからもアクセスできます。Elementノードはオプションで、ルート要素を表します。HTML 文書では(この場合のように)、これは通常はHTMLHtmlElementです。SVG 文書では、これは通常はSVGSVGElementです。このノードはDocumentノードのdocumentElementプロパティからもアクセスでき案す。
DocumentType ノードは常に葉ノードです。Element ノードは、文書コンテンツの大部分が表される場所です。その下のそれぞれの要素、たとえば <head>, <body>, <p> も、Element ノードで表現されます。実際には、それぞれが HTML 仕様で定義された、そのタグ名に特化した Element のサブクラスです。たとえば HTMLHeadElement や HTMLBodyElement などです。これらのサブクラスには、その要素の意味を表す追加のプロパティやメソッドがありますが、ここでは DOM の共通の動作に焦点を当てます。Element ノードは、他の Element ノードを子として持つことができ、入れ子になった要素を表します。例えば、<head> 要素には 3 つの子要素があります。2 つの <meta> 要素と 1 つの <title> 要素です。さらに、要素はテキストコンテンツを表す Text ノードや CDATASection ノードを子要素として持つこともできます。例えば、<p> 要素には単一の子要素として Text ノードがあり、そこには文字列 "This is a paragraph." が設定されています。Text ノードと CDATASection ノードは常に葉ノードです。
子を持つことができるすべてのノード (Document, DocumentFragment, Element) は、2 種類の子を持つことができます。Comment ノードと ProcessingInstruction ノードです。これらのノードは常に葉ノードです。
それぞれの要素は、子ノードに加えて、Attr ノードで表される属性を持つことができます。Attr は Node インターフェイスを継承していますが、主要なツリー構造の一部にはなりません。子ノードを持つことがなく、親ノードが常に null だからです。その代わり、これらは名前付きノードマップに格納され、Element ノードの attributes プロパティからアクセスできます。
Node インターフェイスでは、ノードの型を示す nodeType プロパティを定義しています。要約すると、以下のノード型を導入しています。
| ノード型 | nodeType 値 |
有効な子ノード(Comment と ProcessingInstruction 以外で) |
|---|---|---|
Document |
Node.DOCUMENT_NODE (9) |
DocumentType, Element |
DocumentType |
Node.DOCUMENT_TYPE_NODE (10) |
なし |
Element |
Node.ELEMENT_NODE (1) |
Element, Text, CDATASection |
Text |
Node.TEXT_NODE (3) |
なし |
CDATASection |
Node.CDATA_SECTION_NODE (4) |
なし |
Comment |
Node.COMMENT_NODE (8) |
なし |
ProcessingInstruction |
Node.PROCESSING_INSTRUCTION_NODE (7) |
なし |
Attr |
Node.ATTRIBUTE_NODE (2) |
なし |
メモ:
いくつかのノード型を飛ばしたのに気が付いたかもしれません。Node.ENTITY_REFERENCE_NODE (5), Node.ENTITY_NODE (6), Node.NOTATION_NODE (12) の値は既に使われなくなっており、Node.DOCUMENT_FRAGMENT_NODE (11) の値は DOM ツリーの構築と更新で導入します。
各ノードのデータ
それぞれのノード型は、保持するデータを独自の方法で表します。Node インターフェイス自体には、データに関連する 3 つのプロパティが定義されており、以下の表に要約します。
| ノード型 | nodeName |
nodeValue |
textContent |
|---|---|---|---|
Document |
"#document" |
null |
null |
DocumentType |
その name ("html" など) |
null |
null |
Element |
その tagName ("HTML", "BODY"` など) |
null |
すべての子孫のテキストノードをツリー順に結合したもの |
Text |
"#text" |
その data |
その data |
CDATASection |
"#cdata-section" |
その data |
その data |
Comment |
"#comment" |
その data |
その data |
ProcessingInstruction |
その target |
その data |
その data |
Attr |
その name |
その value |
その value |
Document
文書 (Document) ノード自体は何もデータを持たないため、その nodeValue と textContent は常に null です。その nodeName は常に "#document" です。
Document は、環境(例えば、文書を提供した HTTP レスポンス)から取得される、文書に関するいくつかのメタデータを定義します。
URLプロパティとdocumentURIプロパティは、この文書の URL を返します。characterSetプロパティは、この文書で使用される文字エンコーディング、例えば"UTF-8"を返します。compatModeプロパティは、この文書のレンダリングモードを返します。"CSS1Compat"(標準モード)または"BackCompat"(後方互換モード)です。contentTypeプロパティはこの文書のメディア型を返します。HTML 文書の場合は"text/html"などです。
DocumentType
文書内の文書型 (DocumentType) は、このようになっています。
<!doctype name PUBLIC "publicId" "systemId">
指定することができる部分が 3 つあり、これは DocumentType ノードの 3 つのプロパティに対応します。name, publicId, systemId です。HTML 文書では、doctype は常に <!doctype html> であるため、name は "html" であり、publicId と systemId はどちらも空文字列です。
Element
文書内の要素 (Element) は、このようになっています。
<p class="note" id="intro">This is a paragraph.</p>
コンテンツのほかに指定可能な部分は、タグ名と属性の 2 つがあります。タグ名は Element ノードの tagName プロパティに対応し、この例では "P" です(HTML 要素では常に大文字であることに注意してください)。属性は、Element ノードの attributes プロパティに格納される Attr ノードに対応します。属性については要素とその属性の節で詳しく説明します。
Element ノード自体はデータを保持しないため、その nodeValue は常に null です。その textContent は、ツリー順に並べたすべての子孫テキストノードを連結したもので、この場合 "This is a paragraph." となります。次の要素の場合、
<div>Hello, <span>world</span>!</div>
textContent は "Hello, world!" となります。テキストノードの "Hello, " と、<span> 要素内にあるテキストノードの "world"、それにテキストノード "!" を連結したものです。
CharacterData
Text, CDATASection, Comment, ProcessingInstruction はいずれも CharacterData インターフェイスから継承されており、これはさらに Node のサブクラスです。CharacterData インターフェイスは唯一のプロパティとして data を定義しています。これはノードのテキストコンテンツを保持します。data プロパティもこれらのノードの nodeValue プロパティと textContent プロパティを実装するために使用されます。
Text と CDATASection については、data プロパティがノードのテキストコンテンツを保持します。以下の文書をご覧ください(なお、これは SVG 文書を使用しています。HTML は CDATA セクションを許可していないからです)。
<text>Some text</text>
<style><![CDATA[h1 { color: red; }]]></style>
<text> 要素の中のテキストノードは "Some text" を data として保持しており、<style> 要素の中の CDATA セクションは "h1 { color: red; }" を data として保持しています。
Comment では、data プロパティはコメントのコンテンツのうち <!-- の後から始まり --> の前で終わる部分を保持しています。例えば、次の文書の場合、
<!-- This is a comment -->
このコメントノードは " This is a comment " を data として保持しています。
ProcessingInstruction において、data プロパティは処理命令のコンテンツを保持します。このコンテンツはターゲットの直後から始まり、?> の直前で終わります。例えば、次の文書では、
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
処理命令ノードは 'type="text/xsl" href="style.xsl"' を data として、"xml-stylesheet" を target として保持しています。
さらに、CharacterData インターフェイスは、data 文字列の長さを返す length プロパティと、data の部分文字列を返す substringData() メソッドを定義します。
Attr
次の要素について、
<p class="note" id="intro">This is a paragraph.</p>
<p> 要素には 2 つの属性があり、2 つの Attr ノードで表されます。それぞれの属性は名前と値で構成され、それぞれ name プロパティと value プロパティに対応します。最初の属性は "class" を name とし、"note" を value として保持します。一方、2 つ目となる属性は "id" を name とし、"intro" を value として保持します。
要素とその属性
先述の通り、Element ノードの属性は Attr ノードで表され、これらは別個の名前付きノードマップに格納されます。このマップは Element ノードの attributes プロパティ経由でアクセス可能です。この NamedNodeMap インターフェイスは、以下の 3 つの重要なプロパティを定義しています。
length。これは属性の数を返します。item()メソッド。これは指定された位置のAttrを返します。getNamedItem()メソッド。これは指定された名前のAttrを返します。
Elementインターフェイスは、名前付きノードマップにアクセスすることなく、属性と直接操作するためのいくつかのメソッドも定義しています。
element.getAttribute(name)は、属性が存在すれば、element.attributes.getNamedItem(name).valueと等価です。element.getAttributeNode(name)はelement.attributes.getNamedItem(name)と等価です。element.hasAttribute(name)はelement.attributes.getNamedItem(name) !== nullと等価です。element.getAttributeNames()はすべての属性の名前の配列を返します。element.hasAttributes()はelement.attributes.length > 0と等価です。
属性の所有要素には、Attr ノードの ownerElement プロパティを介してアクセスすることも可能です。
特別な属性として、 id と class は、Element インターフェイス上にそれぞれ、id と className の固有のプロパティを持ちます。これらは対応する属性の値を反映します。さらに、classList プロパティは、class 属性内のクラス一覧を表す DOMTokenList を返します。
要素ツリーでの作業
Element ノードは文書構造の骨格を形成するため、他のノード(Text や Comment など)をスキップして、要素ノードのみを特定して走査することができます。
- すべてのノードにおいて、
parentElementプロパティは、親ノードがElementである場合にその親ノードを返し、親がElementでない場合(例えば親がDocumentである場合)にはnullを返します。これは、親ノードの型に関係なく親ノードを返すparentNodeとは対照的です。 Document,DocumentFragment,Elementについては、childrenプロパティは子ノードのうちElementのみをHTMLCollectionで返します。これは、childNodesがすべての子ノードを返すのとは対照的です。firstElementChildプロパティとlastElementChildプロパティは、それぞれこの集合の最初と最後の要素を返し、子要素がない場合はnullを返します。childElementCountプロパティは、子要素の数を返します。ElementとCharacterDataについては、previousElementSiblingプロパティとnextElementSiblingプロパティはElementである前または次の兄弟ノードを返し、そのような兄弟ノードがない場合はnullを返します。これはpreviousSiblingとnextSiblingが、あらゆる型の兄弟ノードを返す可能性があるのとは対照的です。
ノードの比較
ノードを比較するためには 3 つの重要なメソッドがあります。isEqualNode(), isSameNode(), compareDocumentPosition() です。
isSameNode() メソッドは古いものです。これは厳密等価演算子 (===) のように動作し、2 つのノードが同じオブジェクトである場合のみ true を返します。
isEqualNode() メソッドは、2 つのノードを構造的に比較します。2 つのノードは、同じ型を持ち、同じデータを持ち、かつ各インデックスにおける子ノードも等しい場合に等しいと見なされます。 各ノードのデータの節では、各ノード型に関連するデータを既に定義しています。
Documentについては、データがなく、比較する必要があるのは子ノードだけです。DocumentTypeについては、name,publicId,systemIdの各プロパティを比較する必要があります。Elementについては、tagName(より正確にはnamespaceURI、prefix、localName。これらは XML 名前空間ガイドで紹介します)と属性を比較する必要があります。Attrに対しては、name(より正確にはnamespaceURI、prefix、localName。これらはXML 名前空間ガイドで紹介します)とvalueプロパティを比較する必要があります。- すべての
CharacterDataノード (Text,CDATASection,Comment,ProcessingInstruction) では、dataプロパティを比較する必要があります。ProcessingInstructionについては、targetプロパティも比較する必要があります。
a.compareDocumentPosition(b) メソッドは、ツリー順で 2 つのノードを比較します。これらは相対的な位置関係を示すビットマスクを返します。取りうる場合は次の通りです。
aとbが同じノードであれば0を返します。- 2 つのノードが両方とも同じノードの属性であれば、属性リスト内で
aがbよりも先にある場合はNode.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC(34)、またはaの後にbがある場合はNode.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC(36) を返します。どちらかのノードが属性である場合は、それ以降の比較にはオーナー要素が使用されます。 - 2 つのノードのルートノードが同じでない場合は、
Node.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING(35) またはNode.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_FOLLOWING(37) を返します。どちらが返されるかは実装依存です。 aがbの祖先である場合(bがaの属性である場合も含む)、Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING(10) を返します。aがbの子孫である場合(aがbの属性である場合も含む)、Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING(20) を返します。aがツリー順でbよりも前にある場合、Node.DOCUMENT_POSITION_PRECEDING(2) を返します。aがツリー順でbよりも後にある場合、Node.DOCUMENT_POSITION_FOLLOWING(4) を返します。
ビットマスク値が使用されるため、特定の関係を調べるにはビット単位の AND 演算を使用できます。例えば、a が b より前にあるかどうかを確認するには、次のようにするのが最適です。
if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING) {
// a が b より前にある
}
これは、a と b が同じ要素の属性であり、a が b の祖先であり、かつ a がツリー順序において b より前に位置する場合の状況を説明しています。
まとめ
これまでに導入した機能のすべてをご紹介します。数は多いですが、どれも様々な場面で有益です。
- DOM のすべてのノードは
Nodeインターフェイスを実装しています。 - DOM ツリー内を動き回るには、
parentNode,childNodes,firstChild/lastChild,hasChildNodes(),getRootNode(),previousSibling/nextSiblingを使用します。 - 要素ツリーを動き回るには、
parentElement,children,firstElementChild/lastElementChild,childElementCount,previousElementSibling/nextElementSiblingを使用します。 nodeTypeプロパティはノードの型を示します。nodeName,nodeValue,textContentの各プロパティは、ノードが保持しているデータを提供します。Documentノードと 2 つの重要な子、doctypeとdocumentElement。DocumentTypeノードと 3 つのプロパティname,publicId,systemId。ElementノードとそのプロパティtagName,attributes。Attrノードとそのプロパティnameとvalue。CharacterDataインターフェイスとそのプロパティdata。- 4 つの
CharacterDataのサブクラスText,CDATASection,Comment,ProcessingInstruction。ProcessingInstructionにはtargetプロパティもあります。 - 属性で作業する様々な方法、たとえば
id,className,classListの各プロパティがあります。 - ノードを比較する 3 つのメソッド:
isEqualNode(),isSameNode(),compareDocumentPosition()。