作って納得! DOM 2 Events2007年03月23日 22時14分

ブラウザ上でのプログラミングで避けては通れないのがイベント処理。その仕組みは DOM Level 2 Events にて規定されています。しかし、とりあえず addEventListener メソッドを使ってはいるものの、それがどのような意味を持つか詳しくは知らないといったことはありませんか。そこでここでは、DOM 2 Events のイベントモデルを理解し、ブラウザが裏で何をしているのかを把握するために、実際にそのイベントモデルを実装してみることにします。具体的には、仕様書に定められたインターフェースを JavaScript で実装し、それらを組み合わせてイベントの発生をシミュレートしてみます。

Event インターフェース

まずは Event インターフェースを実装してみましょう。これはイベントリスナに引数として渡されるイベントオブジェクトに相当します。なお、以下インターフェースを実装するオブジェクトには、便宜的に "JS" の接頭辞をつけることとします。

function JSEvent() {
  this.type = "";
  this.target = null;
  this.currentTarget = null;
  this.eventPhase = 0;
  this.bubbles = false;
  this.cancelable = false;
  this.timeStamp = new Date();
  this._propagationStopped = false;
  this._defaultPrevented = false;
}

コンストラクタでプロパティを設定します。各プロパティについてはそのつど説明していきますが、ここで設定する値は一時的なものであって特に意味はありません。ただし timeStamp プロパティには現在時刻を表す Date オブジェクトを設定します。このプロパティはイベントオブジェクトが作成された時間を表すものです。また、仕様にはないけれど内部的に使用するプロパティには、アンダースコアから始まる名前をつけています。

JSEvent.prototype.stopPropagation =
function () {
  this._propagationStopped = true;
};

stopPropagation メソッドはその名のとおりイベントの伝播を停止します。イベントの伝播については後で説明します。このメソッドが呼び出されたからといって即何かが起こるわけではありませんが、呼び出されたことを記録しておくために _propagationStopped プロパティを true に設定しておきます。

JSEvent.prototype.preventDefault =
function () {
  if (this.cancelable)
    this._defaultPrevented = true;
};

イベントの中にはそれに関連付けられたデフォルトアクションを持つものがあります。たとえば、リンク上での click イベントは、リンク先のページに移動するというデフォルトアクションを持ちます。preventDefault メソッドはそうしたデフォルトアクションをキャンセルするためのメソッドです。ただし、すべてのイベントでデフォルトアクションをキャンセルできるわけではないので、キャンセル可能なイベントでのみキャンセルすることを記録しておきます。

JSEvent.prototype.initEvent =
function (eventTypeArg, canBubbleArg, cancelableArg) {
  this.type = eventTypeArg;
  this.bubbles = canBubbleArg;
  this.cancelable = cancelableArg;
};

initEvent メソッドはその名のとおりイベントオブジェクトを初期化します。といってもイベントリスナに渡されるイベントオブジェクトはすでに初期化されているので、普段使用することはあまりなかったかと思います。引数に従ってイベントタイプ ("click""load" など)、バブルするかどうか (後述)、キャンセル可能かどうかを設定します。

JSEvent.CAPTURING_PHASE = 1;
JSEvent.AT_TARGET = 2;
JSEvent.BUBBLING_PHASE = 3;

イベントの発生にはキャプチャリングフェーズ、ターゲットフェーズ、バブリングフェーズの 3 つの段階があります。各段階の詳細については後述しますが、Event インターフェースにはこれらイベントフェーズを表す定数があり、eventPhase プロパティの値として使われます。

EventListener インターフェース

EventListener インターフェースを実装するオブジェクトは実際にイベントを受け取り、それに対する処理を行います。JavaScript では関数オブジェクトがこれに相当します。イベントは引数として受け取り、返り値は必要ありません。

EventTarget インターフェース

EventTarget インターフェースはイベントが発生しうる対象を表します。DOM 2 Events では Node インターフェースを実装するオブジェクト、すなわち要素ノードオブジェクトや文書ノードオブジェクト (document) が EventTarget インターフェースも実装することになっています。

function JSEventTarget() {
  this._listeners = {};
}

コンストラクタで _listeners プロパティを初期化します。このプロパティは、このターゲットに登録されたイベントリスナを格納するハッシュテーブルを表します。

JSEventTarget.prototype.addEventListener =
function (type, listener, useCapture) {
  var listeners = this._listeners[type + !!useCapture];
  if (!listeners)
    listeners = this._listeners[type + !!useCapture] = [];

  // Don't register duplicate listeners
  for (var i = 0; i < listeners.length; i++)
    if (listeners[i] == listener)
      return;

  listeners.push(listener);
};

イベントリスナを登録する、おなじみ addEventListener メソッドです。引数 useCapture は、登録するイベントリスナが呼び出されるイベントフェーズを示しています。実装としては、同一のイベントタイプおよびイベントフェーズで呼び出されるリスナを、配列に格納しておきます。これにより、同じイベントタイプに複数のリスナを関連付けることが可能になります。ただし、あるターゲット上で、ひとつのリスナを同じイベントタイプ、イベントフェーズで二重に登録することはできません。

JSEventTarget.prototype.removeEventListener =
function (type, listener, useCapture) {
  var listeners = this._listeners[type + !!useCapture];
  if (!listeners) return;

  for (var i = 0; i < listeners.length; i++) {
    if (listeners[i] == listener) {
      listeners.splice(i, 1);
      return;
    }
  }
};

removeEventListener メソッドはその名のとおりターゲットからリスナを削除します。ターゲット上に、指定されたイベントタイプ、イベントフェーズと関連付けられたリスナが存在しない場合は何もしません。

JSEventTarget.prototype.dispatchEvent =
function (evt) {
  if (!evt.type)
    throw new JSEventException(JSEventException.UNSPECIFIED_EVENT_TYPE_ERR);

  var targets = [this];
  var ancestor = this;
  while ((ancestor = ancestor.parentNode))
    targets.unshift(ancestor);
  if (evt.bubbles)
    targets.push.apply(targets, targets.slice(0, -1).reverse());

  evt.target = this;
  evt.eventPhase = JSEvent.CAPTURING_PHASE;

  for (var i = 0; i < targets.length; i++) {
    var currentTarget = evt.currentTarget = targets[i];
    var isTargetPhase = (currentTarget == evt.target);
    if (isTargetPhase)
      evt.eventPhase = JSEvent.AT_TARGET;

    var isCapturingPhase = (evt.eventPhase == JSEvent.CAPTURING_PHASE);
    var listeners = currentTarget._listeners[evt.type + isCapturingPhase];
    if (listeners) {
      for (var j = 0; j < listeners.length; j++)
        listeners[j].call(currentTarget, evt);

      if (evt._propagationStopped) break;
    }

    if (isTargetPhase)
      evt.eventPhase = JSEvent.BUBBLING_PHASE;
  }

  var defaultPrevented = evt._defaultPrevented;
  evt.target = null;
  evt.currentTarget = null;
  evt.eventPhase = 0;
  evt._propagationStopped = false;
  evt._defaultPrevented = false;

  return !defaultPrevented;
};

dispatchEvent メソッドはイベントを発生させるメソッドです。あまりなじみがないかもしれませんが、これこそ DOM 2 Events の中枢といってもいいでしょう。実装もボリュームが多めになっていますが、順に見ていくことにします。

まずはイベントタイプをチェックします。イベントタイプが設定されていない、すなわちイベントが初期化されていない場合は例外 (後述) を投げます。

次にリスナが呼び出されるターゲットを取得します。先に言ったようにイベントの発生には 3 つの段階があり、最初のキャプチャリングフェーズでは dispatchEvent メソッドが呼び出されたターゲットの祖先ノード、次のターゲットフェーズは dispatchEvent メソッドが呼び出されたターゲット自身、最後のバブリングフェーズでは再び祖先ノードのリスナが呼び出されます。ただし、祖先ノードは複数存在するので、キャプチャリングフェーズではルートノード (ルート要素ノードではなく文書ノード) から直近の親ノードの順、バブリングフェーズではその逆順でリスナを呼び出していきます。このことを指してイベントが伝播していくともいいます。引数として渡されたイベントがバブルしないものだった場合は、バブリングフェーズは存在しません。

イベントオブジェクトの target プロパティには dispatchEvent メソッドが呼び出されたターゲットを指定します。eventPhase プロパティには最初の段階であるキャプチャリングフェーズを表す定数を指定します。

さて、いよいよ実際にリスナを呼び出す場面です。配列に収めたターゲットを順にたどっていき、それぞれのリスナを取得してさらにそれらを順に呼び出していきます。イベントオブジェクトの currentTarget プロパティには現在呼び出すリスナを持つターゲットを、eventPhase プロパティには適切なイベントフェーズを表す定数を指定します。コードを見てもらえばわかるとおり、addEventListener メソッドで引数 useCapturetrue を指定して登録したリスナは、キャプチャリングフェーズでしか呼び出されません。逆に false を指定して登録したリスナは、ターゲットフェーズまたはバブリングフェーズでしか呼び出されなくなります。また、あるターゲットのリスナをすべて呼び出した後に、それらリスナ中でイベントオブジェクトの stopPropagation メソッドが呼び出されたかどうかをチェックします。もし呼び出されていたのならば、イベントの伝播をそこで中止して後処理に移行します。

後処理ではイベントオブジェクトのプロパティを初期値に戻していきます。また、dispatchEvent メソッドは、イベントにデフォルトアクションが関連付けられており、かつそれがキャンセルされた場合は false を、そうでない場合は true を返します。具体的には、preventDefault メソッドによりデフォルトアクションがキャンセルされていれば _defaultPrevented プロパティが true になっているので、その否定値を返してやります。

DocumentEvent インターフェース

DocumentEvent インターフェースはイベントオブジェクトを作成するためのインターフェースです。通常は文書ノード (document) がこのインターフェースを実装しています。

function JSDocumentEvent() {
  this._constructors = {};
}

イベントオブジェクトのコンストラクタを格納するハッシュテーブルを指定します。

JSDocumentEvent.prototype.createEvent =
function (eventType) {
  if (eventType in this._constructors)
    return new this._constructors[eventType]();

  throw new JSDOMException(JSDOMException.NOT_SUPPORTED_ERR);
};

createEvent メソッドでは引数に従ってイベントオブジェクトを作成します。引数 eventType"click""load" といった個々のイベントタイプではなく、"Event""MouseEvent" といったイベントオブジェクトのインターフェースを表す文字列です。指定されたインターフェースのイベントオブジェクトを作成できない場合は例外 (後述) を投げます。

JSDocumentEvent.prototype._registerEvent =
function (eventType, constructor) {
  this._constructors[eventType] = constructor;
};

ここでは、インターフェースを表す文字列と、そのインターフェースを実装するイベントオブジェクトのコンストラクタを登録するために、_registerEvent メソッドを使用することにします。

DOMException インターフェース

function JSDOMException(code) {
  // DOM
  this.code = code;
  // ECMAScript
  this.message = this._messages[code] || "";
}

JSDOMException.NOT_SUPPORTED_ERR = 9;

JSDOMException.prototype = new Error();
JSDOMException.prototype.constructor = JSDOMException;
JSDOMException.prototype.name = "DOMException";
JSDOMException.prototype._messages = {
  9: "Implementation does not support requested operation"
};

DOMException インターフェースは DOM 2 Core で規定されています。ここでは Error オブジェクトを継承し、例外の種類を表す番号とエラーメッセージをプロパティとして持つことにします。本当はもっと多くの例外が規定されているのですが、DOM 2 Events で使われるのは NOT_SUPPORTED_ERR だけなので、それだけを指定しておきます。

EventException インターフェース

function JSEventException(code) {
  JSDOMException.apply(this, arguments);
}

JSEventException.UNSPECIFIED_EVENT_TYPE_ERR = 0;

JSEventException.prototype = new JSDOMException();
JSEventException.prototype.constructor = JSEventException;
JSEventException.prototype.name = "EventException";
JSEventException.prototype._messages = {
  0: "Event's type is not specified"
};

EventException インターフェースは DOMException インターフェースを継承し、イベントにまつわる例外を表します。DOM 2 Events では UNSPECIFIED_EVENT_TYPE_ERR しか規定されていないので、それだけを実装しておきます。

実際に使ってみる (その 1)

基本的なインターフェースの実装が終わったので実際にイベントモデルをシミュレートしてみることにしましょう。まずは JSEventTarget オブジェクトを継承する JSNode オブジェクトを作ります。

function JSNode(name, parent) {
  JSEventTarget.apply(this);
  this.name = name;
  this.parentNode = parent;
}

JSNode.prototype = new JSEventTarget();
JSNode.prototype.constructor = JSNode;

コンストラクタ内で JSEventTarget コンストラクタを適用するのを忘れないようにしてください。

function logEvent(evt) {
  print("type: " + evt.type + ", " +
        "phase: " + evt.eventPhase + ", " +
        "current target: " + evt.currentTarget.name);
}

var parent = new JSNode("parent", null);
var self = new JSNode("self", parent);

parent.addEventListener("foo", logEvent, true);
parent.addEventListener("foo", logEvent, false);
self.addEventListener("foo", logEvent, true);
self.addEventListener("foo", logEvent, false);

とりあえずは親と子を作り、それぞれにイベントリスナを追加します。

var doc = new JSDocumentEvent();
doc._registerEvent("Event", JSEvent);

イベントオブジェクトを作成するための下準備です。イベントタイプ名 "Event"JSEvent オブジェクトを結び付けます。

var evt = doc.createEvent("Event");
evt.initEvent("foo", true, false);
self.dispatchEvent(evt);

そして実際にイベントオブジェクトを作成し初期化、self 上でそのイベントを発生させます。イベントは parent (キャプチャリングフェーズ)、self (ターゲットフェーズ)、parent (バブリングフェーズ) と伝播し、以下のような出力が得られます。

type: foo, phase: 1, current target: parent
type: foo, phase: 2, current target: self
type: foo, phase: 3, current target: parent

実際に使ってみる (その 2)

別の例としてタイマーを作ってみましょう。このタイマーは一定の時間ごとに timer イベントを発生させます。timer イベントはキャンセル可能であり、キャンセルするとそれ以降の timer イベントは発生しません。また、何度イベントが発生したかを示す count プロパティを持ちます。

function TimerEvent() {
  JSEvent.apply(this);
  this.count = 0;
}

TimerEvent.prototype = new JSEvent();
TimerEvent.prototype.constructor = TimerEvent;

TimerEvent.prototype.initTimerEvent =
function (eventTypeArg, canBubbleArg, cancelableArg, countArg) {
  this.initEvent(eventTypeArg, canBubbleArg, cancelableArg);
  this.count = countArg;
};

TimerEvent オブジェクトは JSEvent オブジェクトを継承し、さらに initTimerEvent メソッドを持っています。DOM 2 Events でも、MouseEvent インターフェースには initMouseEvent メソッドといったように、Event インターフェースから派生するインターフェースは、初期化のための専用のメソッドを備えています。

function Timer(interval) {
  JSEventTarget.apply(this);
  var count = 0;
  var self = this;
  setTimeout(function () {
    var evt = self._doc.createEvent("TimerEvent");
    evt.initTimerEvent("timer", true, true, ++count);
    var doesContinue = self.dispatchEvent(evt);
    if (doesContinue)
      setTimeout(arguments.callee, interval);
  }, interval);
}

Timer.prototype = new JSEventTarget();
Timer.prototype.constructor = Timer;

Timer.prototype._doc = new JSDocumentEvent();
Timer.prototype._doc._registerEvent("TimerEvent", TimerEvent);

Timer オブジェクトは指定時間後に timer イベントを発生させます。また、このイベントにはタイマーを続けるというデフォルトアクションがあります。このデフォルトアクションはキャンセル可能なので、dispatchEvent メソッドの返り値が true のときのみ実行することになります。

var timer = new Timer(1000);
timer.addEventListener("timer", function (evt) {
  print(evt.count);
  if (evt.count == 5) evt.preventDefault();
}, false);

この例だと、1 から 5 までの数値が 1 秒ごとに出力されます。

デモ

「実際に使ってみる」その 1 とその 2 をあわせたデモを作りました。実際にイベントが伝播するさまを確かめてください。

まとめ

このように、イベントを発生させるためには、イベントオブジェクトを作成し、それを初期化し、発生源となるターゲットの dispatchEvent メソッドを呼び出してやる必要があります。これは組み込みのイベントでも同じであり、私たちが普段 load イベントや click イベントを受け取っている裏では、ブラウザがこれら一連の操作を行っている (あるいは行ったかのように振舞っている) のです。また、この仕組みがわかっていれば、Firefox のように EventTarget インターフェースを実装した XMLHttpRequest オブジェクトを、ほかのブラウザでも実装するといったこともできるようになります。

この実装の注意点

DOM 2 Events では、現在処理中のターゲットに追加されたリスナは、そのフェーズでは呼び出されないことになっています。また、現在処理中のターゲットから削除されたリスナが呼び出されることもありません。しかし、コードを簡単にするため、この実装ではこれらの動作が起こることを想定していません。

DOM 2 Events ECMAScript Language Binding では、イベントオブジェクトの timeStamp プロパティの型は Date オブジェクトであるとされています。しかし、多くのブラウザではミリ秒数を表す数値型として実装されており、DOM 3 Events 草案でも数値型に変更されました。この実装では DOM 2 Events に従い、Date オブジェクトをその値に使用しています。

Event インターフェースを実装するイベントオブジェクトを作成するために createEvent メソッドに渡す文字列は、DOM 2 Events では "HTMLEvents" ですが、DOM 3 Events 草案では "Event" になっています。ここでは DOM 3 Events 草案に従い、文字列 "Event" を渡すと JSEvent オブジェクトが作成されるようにしました。

DOM 2 Events では、dispatchEvent メソッドの返り値は、preventDefault メソッドが呼び出されていたら false、呼び出されていなかったら true となっています。Opera 9 はこれに従っていますが、Firefox 2 および Safari 2 は、イベントがキャンセル可能でない場合 (そもそもその場合 preventDefault メソッドの呼び出しは意味を持ちませんが)、リスナが preventDefault メソッドを呼び出していても true を返します。ここでは Firefox 2 および Safari 2 に従い、preventDefault メソッドが呼び出されており、かつその呼び出しが意味を持つ場合のみ、dispatchEvent メソッドが false を返すようにしています。

また、各ブラウザの実装との差異については、「DOM Events とブラウザの実装」も参考になるかと思います。

DOM Events とブラウザの実装2007年03月23日 22時15分

ブラウザ上でのイベント処理の仕組みは DOM 2 Events および DOM 3 Events 草案にて規定されています。しかし、DOM 2 Events で言及されていない部分など、細かい動作はブラウザごとに異なっていることもあります。そうした仕様と実装の差異を、「作って納得! DOM 2 Events」で触れなかったものも含めて、いくつかまとめてみました。

ターゲットフェーズで呼び出されるリスナ

DOM 2 Events のイベントモデルにおいて、あるノードでイベントが発生すると、そのノードの祖先ノードのイベントリスナが呼び出されるキャプチャリングフェーズ、そのノード自身のイベントリスナが呼び出されるターゲットフェーズ、再び祖先ノードのイベントリスナが呼び出されるバブリングフェーズと、3 段階にわたってイベントが伝播していきます。このうちターゲットフェーズでは、addEventListener メソッドで第 3 引数 useCapturefalse に指定して登録したイベントリスナのみが呼び出されることになっています。

Opera 9 はこれに従っていますが、Firefox 2 および Safari 2 ではターゲットフェーズにおいて、useCapturetrue に指定して登録したイベントリスナまでもが呼び出されてしまいます (参考: Mozilla Bug 235441)。なお、Safari はナイトリービルドにおいて一旦は仕様どおりの動作になったものの、Gecko との互換性を保つためにあえて以前の動作に戻したようです。

EventListener インターフェースを実装するオブジェクト

DOM 2 Events において、EventListener インターフェースを実装するオブジェクトは handleEvent メソッドを実装しなくてはいけません。しかし、これは Java のような、関数をファーストオブジェクトとして扱えない言語を考慮に入れてのことであり、付録の ECMAScript Language Binding によれば、ECMAScript では関数オブジェクトが EventListener インターフェースを実装することになっています。すなわち、関数を addEventListener メソッドの第 2 引数 listener などに直接渡せるということです。

ですが、Firefox 2 や Opera 9 では、関数オブジェクトでなくとも handleEvent メソッドを持つオブジェクトなら、EventListener インターフェースを実装するものとして扱ってくれます。Safari 2 では関数オブジェクトしか EventListener として使えませんが、Safari のナイトリービルドでは handleEvent メソッドを持つオブジェクトも EventListener として使えるようです。

var listener = {
  handleEvent: function (evt) { alert(this); },
  toString: function () { return "[EventListener implementation]"; }
};
// Click, and alerts "[EventListener implementation]".
// OK: Firefox 2, Opera 9, and maybe Safari 3
element.addEventListener("click", listener, false);

イベントの伝播におけるルートノード

あるノードでイベントが発生すると、キャプチャリングフェーズではルートノードから直近の親ノードまで、バブリングフェーズでは親ノードからルートノードまでのイベントリスナが呼び出されます。ここで、ルートノードというのは、通常は文書ノード (document) のことを指します。つまり、ある文書中の要素ノードでイベントが発生した場合、キャプチャリングフェーズは document から始まり、バブリングフェーズは document で終わるといった具合です。

Opera 9 はこのとおりの実装になっていますが、Firefox 2 および Safari 2 では window オブジェクトがルートノードの役割を果たしています。すなわち、キャプチャリングフェーズは window から始まり document、ルート要素ノードと続いていき、逆にバブリングフェーズは window で終わるのです。

複数のイベントリスナの実行順

DOM 2 Events では、addEventListener メソッドで複数のイベントリスナが同じノード上に登録されたとき、それらをどういう順番で呼び出すかは規定されていません。しかし Firefox 2、Opera 9、Safari 2 はいずれも、登録されたのと同じ順番でイベントリスナを呼び出します。また、DOM 3 Events 草案でも、addEventListener メソッドで登録されたイベントリスナは登録順で呼び出されることになっています。

イベント発生中のイベントリスナの編集

DOM 2 Events では、あるノードがイベントを処理している間にそのノードに追加されたイベントリスナは、その処理中には呼び出されないことになっています。ただし、イベントフェーズが異なれば呼び出されるので、キャプチャリングフェーズ中に追加されたリスナなら、バブリングフェーズでは呼び出されます。また、あるノードがイベントを処理している間にそのノードから削除されたイベントリスナは、決してその処理中に呼び出されることはありません。

Firefox 2 および Safari 2 はこれを仕様どおり実装していますが、Opera 9 ではイベント処理中に現在のターゲットノードから削除されたイベントリスナも呼び出されてしまいます。

イベントリスナ関数内の this の値

JavaScript では関数オブジェクトをそのままイベントリスナとして使えますが、これが呼び出されたときの関数内の this が指す値は DOM 2 Events では規定されていません。Web Applications 1.0 には EventListener インターフェースに関して In the ECMAScript binding itself, however, the handleEvent() method of the interface is not directly accessible on Function objects. Such functions, when invoked, must be called in the global scope. とありますが、これは this の値が window オブジェクトになるということでしょうか?

実際には、Firefox 2、Opera 9、Safari 2 いずれも this の値は現在イベントを処理中のノード、すなわち引数として渡されたイベントオブジェクトの currentTarget プロパティの値と等しくなります。Firefox 2 ではテキストノード上のイベントリスナに関して、this の値がイベントリスナ関数自身になってしまうバグがありますが、ナイトリービルドではすでに修正されています。また、Safari 2 では window オブジェクト上のイベントリスナに渡されるイベントオブジェクトの currentTarget プロパティの値が、キャプチャリングフェーズでは null、バブリングフェーズでは document オブジェクトになります。

余談ですが、上の引用をする際、Web Applications 1.0 の謝辞Taken さんが載っていることに気づきました。一人だけ漢字なのでやたら目立ちますね。

createEvent メソッドの引数

イベントオブジェクトを作成するためには文書ノードの createEvent メソッドを呼び出します。その際第 1 引数 eventType に渡す文字列は、DOM 2 Events では "HTMLEvents""UIEvents" などとなっていますが、DOM 3 Events 草案では "Event""UIEvent" などに改められています (互換性のために DOM 2 Events の形式も認められています)。

Firefox 2、Opera 9 は DOM 2 Events および DOM 3 Events 草案の両形式に対応していますが、Safari 2 は DOM 2 Events の形式にしか対応していません。つまり、Safari 2 で基本的な Event インターフェースのみを実装するイベントオブジェクトを作成するためには、document.createEvent("HTMLEvents") とする必要があります。なお、Safari のナイトリービルドでは DOM 3 Events の形式にも対応しているようです。

イベントオブジェクトの timeStamp プロパティ

DOM 2 Events において、Event インターフェースの timeStamp プロパティは、基準時刻からの経過ミリ秒数を表し、ECMAScript Language Binding ではその型は Date 型であるとされています。ただし、基準時刻ははっきりとは決められておらず、例としてシステムの開始時刻や Unix epoch (協定世界時 1970 年 1 月 1 日 0 時 0 分 0 秒) が挙げられています。DOM 3 Events 草案では基準時刻は Unix epoch であるとはっきり定められ、ECMAScript Language Binding での型も Number 型に変更されました。また、DOM 2 Events および DOM 3 Events 草案双方で、時間情報が利用できない場合は 0 を返すと規定されています。

各ブラウザの実装はというと、Firefox 2、Opera 9、Safari 2 のすべてにおいて timeStamp プロパティの型は Number 型になっています。ただし、その値はばらばらで、Windows 版 Firefox 2 ではイベントにより、OS 起動時からの経過ミリ秒数を返すもの、0 を返すもの、どこから取ってきたのかわからない数値を返すものがあるようです。Windows 版 Opera 9 では常に 0 を返すようです。Safari 2 がどんな数値を返しているのかはよくわかりませんが、Unix epoch からの経過ミリ秒数ではないようです。

検証

以上の点を確認するためのサンプルを作りました。実際に各ブラウザでの違いを確かめてみてください。