getElement(s)系で対象の要素がない場合の返り値

12月にも関わらず未だコートを出していないyukiinu2ndです。
12月も半ばになりますが、本当に冬とは思えない暖かさですね。
今回もJavaScriptネタです。

要素の有無をチェックし、あれば作成した要素を差し込む、ということをしようとしていました。

//要素を作る
var elem = document.createElement("div");
elem.id = "test";
//差し込み先の取得
var targetElem = document.getElementById("targetdiv");
//要素があれば差し込む
if(typeof targetElem != "undefined"){
	targetElem.appendChild(elem);
}

ところが、これはうまく動きません……。
要素がないにも関わらず、要素の有無を確認しているifの条件を満たしてしまいます。
原因は要素が無い時の返り値が「null」であることです。
このnullですが、undefinedとは別物です。
ちなみに、いずれもif内部で評価するとfalseとなります。
つまり、わざわざtypeofで調べたり、nullであることを確認する必要はありません。

//要素を作る
var elem = document.createElement("div");
elem.id = "test";
//差し込み先の取得
var targetElem = document.getElementById("targetdiv");
//要素があれば差し込む
if(targetElem){
	targetElem.appendChild(elem);
}

undefinedが返ってくると思い込んでいたので、ちょっと悩んでしまいました。
でも、これで解決です。ちゃんと動きます。

参考にしたURL

下記URLにnullやundefined等の判定について詳しく書かれています。
今回引っかかった際に参考になりました。ありがとうございました。
http://d.hatena.ne.jp/unageanu/20070705

getElementBy〜を色々作ってみたのですが……

最近、急に寒くなってついに毛布を出したyukiinu2ndです。
久々に(?)JavaScriptネタです。

所用で「getElementByTagNameFromChildNodes」のような自作のDOM取得メソッドを作ったりしていました。
以下のようにこのままグローバルに置いたりしてもよいのですが……。

var getElementByTagNameFromChildNodes = function(tagname,pos,from){
	var nodes = from.childNodes;
	var cnt = 0;
	for(var i = 0;i < nodes.length;i++){
		if(nodes[i].tagName == tagname.toUpperCase()){
			if(cnt == pos) break;
			cnt++;
		}
	}
	return nodes[i];
};

これだと他のDOM取得メソッドと「.」でつなげて呼べなくて少し不便です……。

//現実は……
var targetElement = getElementByTagNameFromChildNodes("div",2,document.getElementById("Hoge1"));

//本当はこう呼び出したい!
var targetElement = document.getElementById("Hoge1").getElementByTagNameFromChildNodes("div",2);

そこで、HTML上の要素の元となっているクラスである「HTMLElement」を拡張してみます。
この拡張にはprototypeチェーンと呼ばれる仕組みを使います。
あるオブジェクトがプロパティを参照しようとした際、自身のオブジェクトに参照しようとしたプロパティが無かった場合、そのオブジェクトを生成したオブジェクトのprototypeプロパティの中を探す、という処理を再帰的に行います。
よって、拡張はprototypeプロパティに新しいプロパティを定義することで行えます。
※かなり端折っていることと、これであっているのか正直不安ですが……。間違えていたら指摘お願いします。

HTMLElement.prototype.getElementByTagNameFromChildNodes = function(tagname,pos){
	var nodes = this.childNodes;
	var cnt = 0;
	for(var i = 0;i < nodes.length;i++){
		if(nodes[i].tagName == tagname.toUpperCase()){
			if(cnt == pos) break;
			cnt++;
		}
	}
	return nodes[i];
};
//これで取得できる!
var targetElement = document.getElementById("Hoge1").getElementByTagNameFromChildNodes("div",2);

これで問題なし……といきたいところですが、IEではうまく動きません。
実は、IEではprototypeチェーンが使えないという致命的な問題があるためです……。
対策としては、HTC(HTML Component)を使ったり、documentをラップしたものを作って拡張するなど、様々な方法があるようです。
実際にそこまでは試せなかったですが、機会があれば追ったり試したりしてみようと思います。

perlが無言で処理を終了してしまう

最近冷房付けなくてもいいなぁと思いつつも、長袖はまだ暑いと思うyukiinu2ndです。
タイトルは「何やら技術的な問題・バグを発見したぞ」という感じですが……残念ながらそんなことはありません。
原因はプログラムの書き方でしたし、Perlに限ったことではないものでした。

お話

あるところに、古くから動いていたプログラムがありました。
少しずつ手は入れられていて、比較的問題なく動いていたプログラムです。
そんなプログラムでしたが、どういうわけかたまにエラーも何も出さずに処理を中断してしまう現象が発生していました。
もちろん、例外処理は入れてありますし、printデバッグも試してみたのですが原因はわからないままでした。

Perlでの例外処理

ここで少し話が逸れますが、Perlでは例外処理機構がありません。
といっても、完全にないわけではなく、evalを利用することで実現します。

eval{
	#ここで例外処理が起きそうなプログラムを書いておく
	$timecard->in($timedat);
};
if($@){
	#もし、eval内でエラーが発生した場合、$@にエラー内容が返ってくる
	if($@ eq 'INVALIDTIME'){
		#時刻データがおかしい!
		print STDERR "時刻のデータが不正です!\n";
	}else{
		print STDERR $@;#エラーをそのままエラー出力に
	}
	exit 1;#終了
}

#……略

sub in {
	my ($self,$timedat) = @_;
	if($timedat !~ /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/){
		die "INVALIDTIME";
	}
	#……略
}

このように、eval内に例外が起きそうな処理を書いておき、例外(エラー)が発生したら特殊変数「$@」を確認して例外処理をします。
eval内の処理でdieをすることで例外を投げ、dieした際の文字列で例外処理を分けています。
evalがtryで、if($@)でcatchしてその内部で例外種類によって処理をする、という感じでしょうか。

ちなみに、evalの{}の後ろはセミコロンが必要です。忘れやすいので注意しましょう。

続き

数人が調べてみたものの、原因はわかりませんでした。
調べていく際に見つけた不具合を直しても、この現象は起きる状態が続きました……。
しかし、ある人が気がつきました。

「これ、例外処理握りつぶしてない*1?」

こうして、例外処理部分を直すことで解決しましたとさ。めでたしめでたし。

結局は

正確には握りつぶしていたというよりも、条件に問題があり、何も処理されずに終了するケースがあった、というオチでした。
とりあえず、例外来たらエラーログに出力を行うようにしたので、不具合等があってもすぐに解決できるようになりました。よかったよかった。

ちなみに……

他の言語の例外処理構文try〜catchをPerl上で書けるようにする「Error」というモジュールが存在します。
自分はあまり利用したことがないので、説明は上記CPANページのマニュアルに丸投げします。

*1:例外処理やエラーハンドリングに対して、例外として拾うが何も処理をしないことによってエラー等をなかったことにすること。

たまには山登りも……。

6/26に創立イベントとして高尾山に登ってきました。
※創立祭として毎年創立日になると何かしらのイベントを行っています。
普段あまり運動をしないyukiinu2ndですが、無事に頂上まで登ることができました。
かなり暑かったので相当汗かきましたが、かなり気持ちよかったです。
たまには山登りとかもいいですね。かなりいい気分転換になりますし。


下に撮った写真のうち1枚を貼っておきます。もっと天気がいいと遠くまで見えそうですね。

今日のVim

珍しく連投です。もしかしなくても明日は雨が降るだろうなぁ、と思ったyukiinu2ndです。
今日のVimですが、今回はレジスタについてです。

まず、レジスタについて簡単に説明します。
前回の今日のVimでも書きましたが、レジスタは文字列を記録する領域です。
Vim内で文字だけ記録できるクリップボードがたくさんある、そんなイメージでもよいかもしれません。
レジスタは何種類かあります。

レジスタの種類

ユーザが自由に操作できるレジスタ
アルファベットa〜z

アルファベットa〜zのレジスタはユーザが自由に読み書きできるレジスタです。
後述のレジスタ指定の際、アルファベットの小文字で上書き(空にしてから記録)、大文字で追記となります。
このレジスタ以外はVimやOSにより操作されるため、読むことはできますが書くことはできません。

最後にカットやヤンクした文字列が格納されるレジスタ
数値の0

0のレジスタは最後にカットや削除、ヤンク(コピー)した文字列が入っています。
つまり、意識せず(バッファ指定をせず)「y」「d」したものはここに格納され、「p」するとこのレジスタの内容がペーストされます。

カット・削除されたものが処理された順に格納されるレジスタ
数値の1〜9

1〜9のレジスタはカットや削除によって処理された文字列が格納されています。
カットや削除を繰り返す度に数の小さい方から大きい方へ押し出されていきます。

OSのクリップボード参照
アスタリスク

これまでのレジスタVim内に閉じていましたが、アスタリスクだけは例外で、OSのクリップボードとつながっています。
つまり、他のエディタ等でコピーしたものは*レジスタで取り出せますし、逆に*レジスタに書き込むことでクリップボードに記録し、他のエディタで貼り付けることができます。


その他の記号もいくつかあるのですがここでは省略します。
次に、レジスタの操作についてです。

レジスタ

レジスタ一覧を表示
:display

現在のレジスタ一覧を表示します。
前回紹介したマクロ記録をした後、記録したレジスタを見てみるとキーマクロがどのようになっているのかわかると思います。

レジスタ指定
"<レジスタ><レジスタに対する操作>

"(ダブルクォート)の後に文字を指定することで操作対象のレジスタを指定できます。
レジスタ指定後にカットやヤンクを行えば対象のレジスタへ書き込めますし、ペーストならば対象のレジスタに格納されている文字列をその場に貼り付けます。
上記のレジスタの紹介にもありますが、書き込み対象として指定できるのはアルファベットとOSのクリップボード指定である*(アスタリスク)のみです。
ちなみに、範囲指定とレジスタ指定を組み合わせる場合は範囲指定→レジスタ指定→範囲指定した範囲への操作という順番で行います。
例えば3行選択してaレジスタにカットする場合は

V	行選択開始
2j	カーソルを2行下へ移動
"a	aレジスタに対して記録する
d	選択範囲はカットする

このようになります。


このレジスタを活用することで、文字列をいくつも記憶させることができ、クリップボードを拡張するツールも不要になるためかなり便利です。
ただ、自分の場合どのレジスタに何を記録したのかよく忘れるので、「:display」が欠かせません……。

insertBeforeは親子関係でないと使えない

この前、DOMのinsertBeforeで要素を追加しようとしたのですが、なぜかうまくいかず、エラーが出て困ってしまいました。
ただ、最初からエラーが出ていたわけではなく、突然エラーが出るようになったのでさっぱりでした。
しかし、追っていくとDOMで操作しようとしていたHTMLの構造が変化していることに気がつきました。


以前はこのような構造でした。

<!--略-->
<div id="parent">
	<!--ここに入れたい!-->
	<div id="target">hoge</div>
</div>
<!--略-->
<script type="text/javascript">
<!--
var insElem = document.createElement("div");
insElem.id = "inserted";
insElem.innerHTML = "inserted!";
var parPos = document.getElementById("parent");
var basePos = document.getElementById("target");
parPos.insertBefore(insElem,basePos);
//-->
</script>


変更後はこのようになっていました。

<!--略-->
<div id="parent">
	<!--ここに入れたい!-->
	<div id="wrapper">
		<div id="target">hoge</div>
	</div>
</div>
<!--略-->
<script type="text/javascript">
<!--
var insElem = document.createElement("div");
insElem.id = "inserted";
insElem.innerHTML = "inserted!";
var parPos = document.getElementById("parent");
var basePos = document.getElementById("target");
parPos.insertBefore(insElem,basePos);
//-->
</script>


前者はもちろん正しく動きます。
ただ、後者は場所がおかしいものの入る、ということはなくエラーになってしまいます。
子孫関係ではなく、親子関係でないとinsertBeforeが使えないんですね……。
※よく考えれば子孫関係で良ければ適当にbody等を対象に取ってもよくなってしまいますね。

この辺り、JavaScript標準のDOM操作メソッドだけでは結構面倒なのですが、この前jQueryを使ってみたらあまりの簡単さに驚きました。
諸事情で毎回jQueryを使えるわけではないですが、jQuery等のJavaScriptライブラリも勉強して使っていきたいところです。

今日のVim

気がつけば1ヶ月も間が開いてしまいました……。時間の流れが速く感じるようになったら歳だ、と言われたyukiinu2ndです。
まずは今日のVimですが、今回は操作を記録・再現するマクロの使い方について紹介します。

マクロ

操作を記録する
q<任意小文字アルファベット1文字> の後に操作を行い、さらに q

標準モード中にこの操作を行うと、ステータス行に「-記録中-」といった表示が出るはずです。
ここから、記録したい操作を行い、操作が終わったところで「q」を押すことで記録は完了します。
ちなみに、任意な小文字アルファベット1文字で記録先を決めるのですが、大文字にすると後述の通り追記に変わるので注意です。

操作を追記する
q<任意大文字アルファベット1文字> の後に操作を行い、さらに q

先程の通常の操作記録とは異なり、大文字の場合は指定した記録先へ追記を行います。

操作を再現する
<数字>@<マクロ記録先小文字アルファベット>

記録した操作を実行するには「@」を使います。
繰り返しのために「@」の前に数字で回数を指定することもできます。


マクロはあってもなくても困らない機能ではありますが、使いこなせばかなり便利になる機能の一つです。
是非、活用してみて下さい。


ちなみに、マクロの説明中に出てきた「記録先」のことをレジスタといいます。
このレジスタは実際には文字列を記録する場所でマクロ以外にも使い方があり、アルファベット以外の特殊なレジスタもあるのですが、次回以降に紹介したいと思います。