IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

IE8 の DOM のプロトタイプと Getter/Setter API はどうなるか

ちょっと前に

Microsoft 公式に以下のような発表がありました。
Responding to Change: Updated Getter/Setter Syntax in IE8 RC 1 – IEBlog
また、以下のようなドキュメントも公開されています。

これらの内容での概要を自分なりにまとめてみます。

概略

要点は

  • DOM オブジェクトのプロトタイプが使えるようになる
  • DOM オブジェクトに既存の Getter/Setter API が使えるようになる
  • DOM オブジェクトに ECMAScript 3.1 の Getter/Setter API(PropertyDescriptor)が使えるようになる
  • DOM オブジェクト以外の Getter/Setter は出来ない

ということです。

DOM オブジェクトのプロトタイプが使えるようになる
// Element のプロトタイプに isElement というプロパティを作る。
Element.prototype.isElement = true;

// そうすると
// 要素に isElement というプロパティが出来る
alert(document.body.isElement); // true
alert(document.createElement('div').isElement); // true

// HTMLDocument のプロトタイプに hoge という関数を作る
HTMLDocument.prototype.hoge = function() {
    alert('fuga');
};

// そうすると
// document に hoge という関数が出来る
document.hoge(); // fuga
DOM オブジェクトに既存の Getter/Setter API が使えるようになる
// Element のプロトタイプに innerHTML Setter を取得する
var nativeInnerHTML = Element.prototype.__lookupSetter__('innerHTML');

// Element のプロトタイプに innerHTML Setter を設定する
Element.prototype.__defineSetter__('innerHTML', function(html) {
    alert(this.tagName + 'の innerHTML が設定されました!');
    return nativeInnerHTML.call(this, html);
});

// body の innerHTML に値を設定する
document.body.innerHTML = '<p>Hello, world!</p>'; // 「body の innerHTML が設定されました!」と表示される
DOM オブジェクトに ECMAScript 3.1 の Getter/Setter API(PropertyDescriptor)が使えるようになる
// Element のプロトタイプに innerHTML Setter を取得する
var nativeInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML').setter;

// Element のプロトタイプに innerHTML Setter を設定する
Object.defineProperty(Element.prototype, 'innerHTML', {
    setter: function(html) {
        alert(this.tagName + 'の innerHTML が設定されました!');
        return nativeInnerHTML.call(this, html);
    }
});

// body の innerHTML に値を設定する
document.body.innerHTML = '<p>Hello, world!</p>'; // 「body の innerHTML が設定されました!」と表示される
DOM オブジェクト以外の Getter/Setter は出来ない
// 下記はエラーが発生する
Object.defineProperty(Object.prototype, 'hoge', { setter: function() { alert('fuga') } });  // Error!

以下を参照してください。

The first parameter (the object on which to attach the accessor) supports only DOM instances, interface objects and interface prototype objects in Internet Explorer 8.

Internet Explorer for Developers | Microsoft Docs

ただ、そのうち出来るようにしたい的なことはかかれていました。
当然、オブジェクトリテラル内で定義することは出来ないと思われます。

とりあえず

これで言いたいことは終わりなのですが、もうチョイ詳しいことも書いておきます。

Getter/Setter API の背景と流れ

Getter/Setter API に関しては、ちょっと複雑な経緯があるのでここまでの流れを紹介します。

  1. ECMAScript 3 の仕様が固まる。この時点で Setter/Getter API の仕様は存在しない
  2. Firefox が勝手に Getter/Setter API を実装(これが、さっき紹介した「既存の Getter/Setter API」)
  3. Safari, Opera もそれを追従
  4. IE8 Beta2 もそれを追従
  5. ECMAScript 3.1 で、新しく Getter/Setter の仕組み(PropertyDescriptor)およびその API が仕様化される
  6. IE8 では、既存の API を残しつつ、新しい API を実装することに決定(←イマココ!)
  7. Mozilla, Opera, Apple, Google も ECMAScript 3.1 の仕様に同意しているので、この流れに追従するものと思われる。

こういう流れがあったんですね。

プロトタイプとは何か

これは、何回も書いていると思うのですが何回でも書きます!

プロトタイプとは複数オブジェクトの共通プロパティを持つオブジェクト

プロトタイプとは、複数のオブジェクトの共通プロパティを持つオブジェクトです。
たとえば、 document.body や document.documentElement や document.createElement('div') の共通プロパティを持っているのが Element.prototype というオブジェクトです。
以下の例をみてください。

var body = document.body;
var html = document.documentElement;
var div = document.createElement('div');

Element.prototype.hoge = 1;

alert(body.hoge); // 1
alert(html.hoge); // 1
alert(div.hoge); // 1

Element.prototype.hoge = 2;

alert(body.hoge); // 2
alert(html.hoge); // 2
alert(div.hoge); // 2

簡単ですよね。
これの、 Element.prototype が document.body や document.documentElement や document.createElement('div') のプロタイプです。

共通プロパティの代表はメソッド

この仕組みは主にメソッドで使われます。
たとえば、以下のように Element.prototype にメソッドを作ると body や div でも呼び出せるようになるのです。

var body = document.body;
var html = document.documentElement;
var div = document.createElement('div');

Element.prototype.sayTagName = function() { alert(this.tagName) };

body.sayTagName(); // BODY
html.sayTagName(); // HTML
div.sayTagName(); // DIV

たとえば、ブラウザの実装にもよりますが appendChild や removeChild などのメソッドもプロトタイプで定義されています。

IE で「DOM オブジェクトのプロトタイプを使えるようになる」とはどういうことなのか

今までの IE の特徴は

  • DOM オブジェクトはプロトタイプを持っていない
  • DOM オブジェクトの実装が標準仕様に従っていない(主にイベント周り)

というところでした。
これが IE8 からは

  • DOM オブジェクトがプロトタイプを持っている(New!!)
  • DOM オブジェクトの実装が標準仕様に従っていない(主にイベント周り)

という風になったのです。

それはどういうことか

標準仕様に従っていない部分がプロトタイプに局所化されたということになります。
ということは、 JavaScript を書く人はプロトタイプを標準準拠の API に書き換えてしまうことが出来るようになったのです。
たとえば、以下の記事のように IE に addEventListener を実装することが出来ます。

// Apply addEventListener to all the prototypes where it should be available.
HTMLDocument.prototype.addEventListener =
Element.prototype.addEventListener =
Window.prototype.addEventListener = function (type, fCallback, capture)
{
  var modtypeForIE = "on" + type;
  if (capture)
  {
    throw new Error("This implementation of addEventListener does not support the capture phase");
  }
  var nodeWithListener = this;
  this.attachEvent(modtypeForIE, function (e) {
    // Add some extensions directly to 'e' (the actual event instance)
    // Create the 'currentTarget' property (read-only)
    Object.defineProperty(e, 'currentTarget', {
      getter: function() {
         // 'nodeWithListener' as defined at the time the listener was added.
         return nodeWithListener;
      }
    });
    // Create the 'eventPhase' property (read-only)
    Object.defineProperty(e, 'eventPhase', {
      getter: function() {
        return (e.srcElement == nodeWithListener) ? 2 : 3; // "AT_TARGET" = 2, "BUBBLING_PHASE" = 3
      }
    });
    // Create a 'timeStamp' (a read-only Date object)
    var time = new Date(); // The current time when this anonymous function is called.
    Object.defineProperty(e, 'timeStamp', {
      getter: function() {
        return time;
      }
    });
    // Call the function handler callback originally provided...
    fCallback.call(nodeWithListener, e); // Re-bases 'this' to be correct for the callback.
  });
}

// Extend Event.prototype with a few of the W3C standard APIs on Event
// Add 'target' object (read-only)
Object.defineProperty(Event.prototype, 'target', {
  getter: function() {
    return this.srcElement;
  }
});
// Add 'stopPropagation' and 'preventDefault' methods
Event.prototype.stopPropagation = function () {
  this.cancelBubble = true;
};
Event.prototype.preventDefault = function () {
  this.returnValue = false;
};
Internet Explorer for Developers | Microsoft Docs

「MSDN に書いてる暇があるなら!それを実装して出荷してくれよ!!!」という言葉はかみ締めておきましょう。

プロトタイプだけでは解決できない非互換

DOM のプロパティには代入が特殊な意味を持つということが多々あります。
たとえば、

document.body.innerHTML = '<p>hoge</p><p>fuga</p><p>piyo</p>';

alert(document.body.childNodes.length); // 3

document.body.innerHTML = '<p>hoge</p><p>fuga</p>';

alert(document.body.childNodes.length); // 2

このように、代入すること自体に代入以外の意味があることが多々あるのです。
このようなプロパティに非互換がある場合は、プロトタイプがあるだけでは解決ができません。
どうしても、 Getter/Setter という仕組みが必要なのです。

そもそも Getter/Setter ってなんなのさ!

Getter/Setter とは、プロパティの代入や参照が発生したときに、指定した関数オブジェクトを呼び出す仕組みです。
たとえば、以下のようなものです。

var obj = {};

// hoge という Getter を作る
obj.__defineGetter__('hoge', function() { return 1 });

alert(obj.hoge); // 1

// hoge という Setter を作る
obj.__defineSetter__('hoge', function(v) { this._v = v; return v * 2 });

alert(obj.hoge = 2); // 4
alert(obj._v); // 2

既存の Getter/Setter API の詳細

Firefox, Safari, Opera で使える Getter/Setter API の詳細を説明します。

  • __defineGetter__
  • __defineSetter__
  • __lookupGetter__
  • __lookupSetter__
  • オブジェクトリテラル内で定義
__defineGetter__

ゲッターを作ります。

document.body.__defineGetter__('hoge', function() {
    return 'fuga';
});

alert(document.body.hoge); // fuga
__defineSetter__

セッターを作ります。

document.body.__defineSetter__('hoge', function(v) {
    this.v = v * 2;
    return v;
});

var a = document.body.hoge = 2; // このようにしたときに a にはセッターが返した値が入る

返り値として、受け取った値を返すようにしておかないと式の値がおかしくなるので注意が必要です。

__lookupGetter__

ゲッターを探します。

var innerHTMLGetter = document.body.__lookupGetter__('innerHTML');

// innerHTML を参照したときと同じことができる
innerHTMLGetter.call(document.body);

このとき、 __lookupGetter__ はプロトタイプが持つゲッターも探してくれます。

__lookupSetter__

ゲッターを探します。

var innerHTMLSetter = document.body.__lookupSetter__('innerHTML');

// innerHTML に 'hoge' を代入したときと同じことができる
innerHTMLSetter.call(document.body, 'hoge');

同様に、 __lookupSetter__ はプロトタイプが持つゲッターも探してくれます。

オブジェクトリテラル中に定義

以下のような set/get キーワードを使って、オブジェクトリテラル中で定義することができます。

var hoge = {
    get a() { return this._a * 2 },
    set a(v) { this._a = v; return v } 
};

hoge.a = 1;
alert(hoge.a); // 2

プロトタイプと Getter/Setter API だけでは対応できない微妙な問題

プロトタイプと既存の Getter/Setter API があれば、ほとんどの非互換による問題は解決できるのですが、

  • プロパティが for in されるかされないか
  • プロパティが書き換え可能か
  • プロパティが削除可能か

に依存したコードが存在する場合は、既存の Getter/Setter API では対処することができません。
その代表的な例はオブジェクトをハッシュとして使っている場合などです。
この微妙な問題を解決するためには、

  • プロパティが for in されるかされないか
  • プロパティが書き換え可能か
  • プロパティが削除可能か

というプロパティの属性を設定出来ることが望まれます。
ECMAScript 3.1 の PropertyDescriptor を使うと Getter/Setter を作ることと同時にこれらのプロパティの属性を設定することも出来るようになります。

ECMAScript 3.1 の PropertyDescriptor の詳細

ECMAScript 3.1 では Getter/Setter も、「削除可能か、書き換え可能か、 for in されるか」という属性も、プロパティの値もすべて PropertyDescriptor というもので扱われます。
つまり、プロパティの状態はすべて PropertyDescriptor を使って決めることができます。
この PropertyDescriptor についての詳細は、以前に記事を書いたのでそちらをごらんください。
(注:以下の記事執筆時点で flexible という名前だったプロパティに関しては、現時点で configurable という名前に変更されています。)
次の JavaScript の仕様はこうなる! ECMAScript 3.0 から 3.1 への変更点まとめ - IT戦記