Ember.js はレガシー IE でも動くようになっています、を対応しましたという話

この記事は Ember.js Advent Calendar の1日目です。

やはり Ember.js は大人気のようで、 Advent Calendar もあとたった 22 枠しか空きがありません。(12/1現在)
すこしでも Ember.js に興味がある方は急いで登録されることをオススメします!

この記事では私が Ember.js に送ったのレガシー IE 対応のパッチをご紹介しつつ、IE での JavaScript の罠とその対応を紹介します。
今現在 IE と向き合っている方や、これから IE と向き合うことになる方のご参考になれば幸いです。

ちなみに、 Ember.js は 1.0.0 の時点で IE 6 ですべてのテストケースに通過しており、1.2.2 (現在の最新の安定版) でも IE6 上で動作します。

ここで、レガシーIE が何か、というのを定義しておきましょう。 本記事ではレガシーIE とは IE 6, 7, 8を指すことにします。
そして今回はレガシー IE についてのみ言及しているので、以降は単に「IE」と呼ぶことにします。

IE での罠たち

IE の JavaScript には多くの罠が存在します。
それもそのはず、 IE 9 未満は ECMA-262 ではなく JScript という JavaScript っぽい独自の言語だからです。

なので、まずは現在多くのプラットフォームで動作することが期待されている ECMA-262 3rd edition と互換性を目指すことで IE 対応を進めていきたいと思います。

Array にはメソッドが足りない

ECMA-262 3rd edition で定義されている Array のメソッドの中で、 JScript では定義されていないメソッドがいくつか存在します。

  • Array.prototype.forEach
  • Array.prototype.map
  • Array.prototype.indexOf

そこで、 Ember.js にはこれらの足りないメソッドのために、互換性のある関数群が用意されています。

  • Ember.ArrayPolyfills.forEach
  • Ember.ArrayPolyfills.map
  • Ember.ArrayPolyfills.indexOf

この関数群には、該当する Array にメソッドがすればそのメソッドが、なければ独自に定義した互換関数が格納されています。

ということで、この点についての IE 対応は比較的簡単です。 forEachmap が使われている箇所を、すべて互換関数に置き換えてしまえば対応完了です。

host object にはメソッドが足りない

ホストで提供されている Function には本来 Function オブジェクトが持っているはずのメソッドである toStringapply が定義されていません。

信じられないことに、次のコードは IE では実行時例外となります。

setTimeout.toString();
//=> throw 'Object doesn't support this property or method'

toString に関しては、

String(setTimeout);
//=> 'function() { [native code] }'

で回避できます。

ただし apply については一工夫必要です。

稀に、setTimeout を一時的にオーバーライドして引数をチェックした上で本来の setTimeout に渡したい、とったようなケースがあり、その際には apply をなんとかして使う必要があります。
私は Function.prototype.applysetTimeout に apply することで対応することにしました。

var apply = Function.prototype.apply;
apply.apply(setTimeout, [this, arguments]);

Object.prototype のプロパティと同名のプロパティが for in で列挙されない

通常 for ~ in ループでは、オブジェクト自身が持っているプロパティを列挙することができます。 しかし IE では、Object.prototype で定義されているプロパティと同じ名前のプロパティについては列挙されません。

var object = {
  toString: function() { return 'hi' }
};

for (var key in object) {
  // 一般的な JavaScript とは違い、toString が列挙されない。
}

この対応についてはかなり悩んだ結果、事前に Object.prototype のプロパティ名の配列を用意しておき、ひとつひとつプロパティをチェックするようにしました。

var keys = [
  'constructor',
  'hasOwnProperty',
  'isPrototypeOf',
  'propertyIsEnumerable',
  'valueOf',
  'toLocaleString',
  'toString'
];

for (var i = 0, length = keys.length; i < length; i++) {
  if (object.hasOwnProperty(key)) {
    // ここでは toString が取得できる
  }
}

暗黙の global 変数と window のプロパティは別

IE では 「global 変数 = window のプラパティ」というわけではありません。

greet = 'hi';

window.greet; //=> 'hi'
greet; //=> 'hi'

window.greet = 'bye';

window.greet; //=> 'bye'
greet; //=> 'hi' // ここで一般的なブラウザは 'bye' が変える

globa 変数を使わなければ問題ないように見えますが、もとから存在する global 変数を一時的にスタブしたいといった場合には致命的な挙動となります。 なので、そういったケースであれば、常に window 付きで変数にアクセスするというのが有効です。

その他

他にも、Object.create が不完全な状態で実装されていたり<a> の href に必ずホスト名が含まれたり<input type=checkbox>は focus して blur しないと change イベントが発火しなかったりといった DOM 由来の奇妙な動作がいくつかあるんですが、今回は詳しい説明は割愛します。

感想

苦行とされるIE対応も、やってみると意外な発見がありました。

  • 納期も制約もない IE 対応は意外と楽しい
  • 得られる知見が多く、マルチプラットフォームで動くコードを維持する大変さを体感できる(ただし未来へつながる知見はない)

ちなみに、わたしがなぜ Ember.js の IE 対応を行なっているかというと、もちろん Ember.js 自体の価値になるというのもあるんですが、 一番の理由は過去に経験した IE 対応の案件で大変苦い思いをしたからです。
当時 IE をあまく見すぎていたために、時間・体力・精神力の大部分を持って行かれてしまいました。
案件が始まる前からちゃんと IE と向き合っていられれば、そして当時 IE に対応した Ember.js があれば、何かが違っていたかもしれません。

そこで、「過去の自分を救いに行く作業」を経験された hmsk センパイを見習うことにしたのです。

まとめ

今回ご紹介したのは、実は JavaScript の IE 対応のほんの一部の Tips です。
実際に web サイト/アプリを IE 対応させようとした場合には、 JavaScript 以外にも DOM や CSS も対応させる必要がありますし、 さらにそれを統合した状態で期待通りに動くよう調整を行なう必要があります。

というわけで、IE と向き合っているみなさまにとって、本記事が少しでも手助けになれば幸いです。

では、引き続き Ember.js Advent Calendar をお楽しみくださいませ :-)