スポンサーリンク

JavaScriptの動かないコード (中級編) イベントハンドラが見る値のエラー


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:
 ページが読み込まれた時刻を,ボタン押下で表示する。

<input type="button" id="my_button"
  value="このページが読み込まれた時刻を表示">

<script language="JavaScript">

// onload でイベントハンドラを設定する
window.onload = function(){

	var my_button = document.getElementById( "my_button" );
	my_button.onclick = function()
	{
		// まず,onload時の現在時刻を取得
		var now = new Date();

		// その時刻を表示
		alert( now );
	};

}

</script>




答え



これだと,毎回ボタンが押された時刻を表示してしまう。

ボタンを押すたびに表示時刻は変わる。本当は,ページを最初に読み込んだ時刻をずっと記憶し,毎回同じ一定時刻を表示してほしいのだが。


かりに 18:00:00 にページが読み込まれたとしよう。

コード中で

	// まず,onload時の現在時刻を取得して変数に保管
	var now = new Date();

という部分に注目する。予想では,このonloadの時点で new Date() が評価されて文字列になり,

	var now = "Tue Sep 16 2008 18:00:00 GMT+0900";

という値が代入されるように思える。


しかし,実際にはそうではない。


上のコード中で new Date(); と書いてあっても,ここでは new Date(); は評価実行されない。

計算実行後の「値」ではなく,ただ, new Date() を呼び出すという「計算方式」のみが,イベントハンドラとしてセットされるのである。


いわば,次のようなHTMLと同じであると見ればわかりやすいだろう。

<input type="button" id="my_button" value="表示"
 onclick="var now = new Date(); alert( now );">

この記法ならば,new Date() によって現在時刻が評価されるのはonload時ではなく,onclick時になってからだ,という事がよくわかる。


また,冒頭で挙げたコードは,下記のように書き換えることもできる(※ただし同値ではない*1)。

<input type="button" id="my_button"
  value="このページが読み込まれた時刻を表示">

<script language="JavaScript">

// onload でイベントハンドラを設定する
window.onload = function(){

	var my_button = document.getElementById( "my_button" );
	my_button.onclick = f;
}

function f()
{
	// 現在時刻を取得
	var now = new Date();

	// その時刻を表示
	alert( now );
};

</script>

このように function の中身を名前を持った関数として外側にくくり出してみれば,function(){〜〜} の 〜〜 の部分は onload で実行されるわけがない,という事がよくわかるだろう。


冒頭のコードの場合は,関数の定義内容を外側にくくり出していなかったため,new Date(); というコードがあたかも定義と同時に実行されるかのように感じられてしまったのである。



onload の時点で時刻を保管したい場合は,下記のように,new Date(); による時刻生成のコードをイベントハンドラの外に出して書けばよい。

<input type="button" id="my_button"
  value="このページが読み込まれた時刻を表示">

<script language="JavaScript">

// onload でイベントハンドラを設定する
window.onload = function(){

	// まず,onload時の現在時刻を取得 ←イベントハンドラの外で定義
	var now = new Date();

	var my_button = document.getElementById( "my_button" );
	my_button.onclick = function()
	{
		// 時刻を表示
		alert( now );
	};

}

</script>

ところで,冒頭のコードが見逃していた点は,

function () {〜} のような無名関数記法(もしくは,クロージャ)を使った場合には,その中身はその場ですぐには評価されない

という事であった。


このことは確かにエラーの原因ともなり得るが,実は利点でもある。


下記のサイトでは,ラムダ計算の「遅延評価」の実装として,JavaScriptのクロージャを利用している。

λ Calculus - まずは遅延評価から
http://blog.livedoor.jp/dankogai/arch...

Haskell等の関数型言語を使った事があればわかるが,遅延評価とは「必要になるまで計算しない」事を指す。

function(){〜〜} の中身が遅延評価されることを利用すれば,再帰呼び出しでの無限ループを回避できる。

JavaScriptがλ計算関数プログラミングに活用できる,として注目されるゆえんがここにある。



JavaScriptでクロージャを扱う際の独特の注意例は他にもあり,次の記事で取り上げられる。



 

*1:クロージャを使わないように書き換えたので,クロージャの持つ各性質は失われる。