childNodesの罠

yukiinu2ndです。また間が空いてしまいました……。
#文章にまとめるのは難しいです。

今回は、JavaScriptについて勉強をしている方から相談を受けた(というより自ら首を突っ込んだ)際にはまってしまったことについて紹介します。

その方、何でもJavaScriptからchildNodesなどを使ってDOM操作をすることで動的に要素を操作する、というサンプルを動かしていたのですがうまく動かないと困っていました。
childNodesはノードオブジェクトのプロパティであり、

	var kodomotachi = document.getElementById("oya").childNodes;
	alert(kodomotachi[0].innerHTML);

とするとkodomotachiにid属性が「oya」である要素の子ノードの配列が返ります。
上記のようにその要素の持つ子ノードのHTML等を取得することもできます。

しかし、このchildNodesですが、IEFirefoxで挙動が違う*1ようなのです。
困っていた方のサンプルが動かなかった原因も、この挙動の違いによるものでした。

例えば、以下のようなサンプルを動かしてみます。

<html>
<head>
<title>JS Test</title>
</head>
<body>
<div id="textblock">
	<p>段落1</p>
	<p>段落2</p>
	<div id="textblock2">
		<p>段落3</p>
	</div>
</div>
<script language="JavaScript"><!--
function changeNodes(){
	var target = document.getElementById("textblock");
	target.childNodes[0].innerHTML = "change!\n";
}
//--></script>
<form action="#" method="GET">
<input type="button" value="Check!" onclick="changeNodes()" />
</form>
</body>
</html>

このサンプルは「Check!」ボタンを押すと、1つめのp要素の中身を「change!」に置き換えます。
IEでは意図通りの動作をしますが、Firefoxではぱっと見何も起きません……。
困っていた方のサンプルも上記サンプルとほぼ似たようなことをしていました。

もう一つサンプルを見てみましょう。

<html>
<head>
<title>JS Test</title>
</head>
<body>
<div id="textblock">
	<p>段落1</p>
	<p>段落2</p>
	<div id="textblock2">
		<p>段落3</p>
	</div>
</div>
<script language="JavaScript"><!--
function getNodes(){
	var target = document.getElementById("textblock");
	alert("childNodes.length="+target.childNodes.length);
}
//--></script>
<form action="#" method="GET">
<input type="button" value="Check!" onclick="getNodes()" />
</form>
</body>
</html>

このサンプルを実行し、「Check!」ボタンを押すとtextblockのid属性を持つdiv要素の子ノード数をAlertで表示します。
「p要素が2つ、div要素が1つ子としてあるから3が返るだろう!」と考える人も多いと思います。自分も調べるまではそうでした。
実際に、IEで実行すると答えは3で返ります。
しかし、FirefoxSafariで実行するとなぜか7が返ります。7とは何とも中途半端な数字です……。

では、この子ノード数の3個と7個の違いは何かを見てみます。

<html>
<head>
<title>JS Test</title>
</head>
<body>
<div id="textblock">
	<p>段落1</p>
	<p>段落2</p>
	<div id="textblock2">
		<p>段落3</p>
	</div>
</div>
<script language="JavaScript"><!--
function getNodes(){
	var target = document.getElementById("textblock");
	var tmplog = "";
	for(var i = 0;i < target.childNodes.length;i++){
		tmplog += target.childNodes[i].nodeName+","+target.childNodes[i].nodeValue+","+target.childNodes[i].nodeType+"\n";
	}
	document.getElementsByTagName("body")[0].innerHTML += tmplog;
}
//--></script>
<form action="#" method="GET">
<input type="button" value="Check!" onclick="getNodes()" />
</form>
</body>
</html>

このサンプルは「Check!」ボタンを押すと、HTMLの末尾に子ノードの簡単な情報を表示します。
一行一ノードとなっていて、左からカンマ区切りで

  1. ノード名(nodeName)
  2. ノート値(nodeValue)
  3. ノード種別(nodeType)

を表示します。
実行すると、次のような結果になります。
まず、IEで動かした結果です。

P,null,1
P,null,1
DIV,null,1

これは予想通りの内容です。
次に、Firefox2で実行してみます。

#text, ,3
P,null,1
#text, ,3
P,null,1
#text, ,3
DIV,null,1
#text, ,3

何かIEとは違うノードが存在するようです。
ノード名が「#text」となっていて、ノード種別も3になっています。
ノード種別について調べてみると、1は要素、3はテキストとなっています。
テキストノードはその名の通り文字列情報を持つノードです。要素に囲まれていない文字列がこのノードになるようです。(この辺りは正確かどうか自信がないですが……)
これは、勝手にFirefoxが謎の空白文字を含んだテキストノードを追加しているのか……。
それを確かめるために以下のようにサンプルを変えてみます。

<html>
<head>
<title>JS Test</title>
</head>
<body>
<div id="textblock">
	1つめ
	<p>段落1</p>
	2つめ
	<p>段落2</p>
	3つめ
	<div id="textblock2">
		<p>段落3</p>
	</div>
	4つめ
</div>
<script language="JavaScript"><!--
function getNodes(){
	var target = document.getElementById("textblock");
	var tmplog = "";
	for(var i = 0;i < target.childNodes.length;i++){
		tmplog += target.childNodes[i].nodeName+","+target.childNodes[i].nodeValue+","+target.childNodes[i].nodeType+"\n";
	}
	document.getElementsByTagName("body")[0].innerHTML += tmplog;
}
//--></script>
<form action="#" method="GET">
<input type="button" value="Check!" onclick="getNodes()" />
</form>
</body>
</html>

まず、Firefox2の結果。

#text, 1つめ ,3
P,null,1
#text, 2つめ ,3
P,null,1
#text, 3つめ ,3
DIV,null,1
#text, 4つめ ,3

予想通り入力した文字が入っています。
では、IEの結果はどうでしょうか。

#text,1つめ ,3
P,null,1
#text,2つめ ,3
P,null,1
#text,3つめ ,3
DIV,null,1
#text,4つめ ,3

……きちんと文字列が格納されています。
どうも、IEの場合、空白文字のみで構成されている文字列に関しては切り捨てる(もしくはどこか別の要素に入っている?)処理になるようです。

これは、childNodesを基準にDOM操作をする時はブラウザによる挙動の違いに気をつけないといけないようです。
まさか、こんな罠があるとは思いませんでした……。

*1:全てのブラウザで確認していないので断言はできないのですが、IEFirefoxなどそれ以外のブラウザ(Safari3系など)で挙動が分かれるようです。