Object.keys等のstaticメソッドがES6で挙動が変わったことで面倒なことになってる話
ES5でプリミティブを受け取ると例外を投げていたObject.keys
等のメソッドが、ES6では例外を吐かなくなった。
Object.getPrototypeOf ( O )
Object.getOwnPropertyDescriptor ( O, P )
Object.getOwnPropertyNames ( O )
Object.seal ( O )
Object.freeze ( O )
Object.preventExtensions ( O )
Object.isSealed ( O )
Object.isFrozen ( O )
Object.isExtensible ( O )
Object.keys ( O )
各ES5/6仕様書へのリンク: https://gist.github.com/teppeis/5284739aa1c6823b2ea9
- ES6 compat tableの Misc > Object static methods accept primitives で見れる
- プルリク投げてメソッド毎の対応状況を見れるようにした (compat-table/compat-table#391)
Firefox, Chromeは今週ぐらいにリリースされるバージョンから反映される。
- SpiderMonkey
- Firefox 33:
Object.getOwnPropertyNames
のみ対応 - Firefox 35: 全メソッド対応 (Bug 1038545)
- Firefox 33:
- V8:
Object.getOwnPropertyNames
とObject.keys
のみ対応 (issue3443)- Chrome 40
- io.js
(注: Node.js自体はv8のバージョンが古いためv0.11/v0.12では反映されない。io.jsはv8をアップデートしてるので反映されてる。)
これらメソッドの投げる例外に依存してプリミティブとオブジェクトを判別しているロジックは壊れる。
影響範囲の大きいところとしては、Node.jsのassert.deepEqual
。
// https://github.com/joyent/node/blob/v0.11.14/lib/assert.js#L213
try {
var ka = Object.keys(a),
kb = Object.keys(b),
key, i;
} catch (e) {//happens when one is a string literal and the other isn't
return false;
}
Node.js自体はv0.11/0.12が採用するv8のバージョンが古いためにassert
が壊れることはないが、これをフォークしたio.jsはv8を上げてしまったので壊れた。
また、Node.jsのassertから派生した各種ブラウザ用アサーションライブラリも壊れた。
- io.js
- commonjs-assert (browserifyが置換するassert互換パッケージ)
- power-assert (browserify経由のcommonjs-assert依存)
- Jxck/assert
- expect.js
これによって、プリミティブとオブジェクトの比較、具体的には以下のようなassertが通ってしまうので致命的。
assert.deepEqual(1, {}); // should fail, but actually pass...
独自ロジックでdeepEqual相当の処理をしてるChaiやJasmineは大丈夫だった(逆に言うと、これらはCommonJS AssertのdeepEqualの仕様とは異なる)。
以下はプルリクを送って直した。
- io.js (nodejs/node#193)
- commonjs-assert (browserify/commonjs-assert#7)
- browserify v8.1.0に取り込まれたので、依存するpower-assertも直った。
expect.jsも直さないと。。
これまでcommonjs-assertは、プリミティブに対して例外を吐かないやる気のないObject.keysのshim実装を使っていたため、このshimが使われるIE8では上記の例外に依存するロジックが正しく動作せず、assert.deepEqual
でプリミティブとオブジェクトを正しく比較できないという致命的な状態だった。
今回の対応で例外に依存しないロジックになったため、Object.keysのshimは相変わらずやる気が無いものの、致命的な欠陥は無くなった。
現存するcommonjs-assertのIE8でのバグはおそらく以下。
assert.deepEqual
にarguments
を渡す比較が正しくないassert.deepEqual
にtoString
などのnot-enumarableなプロパティを持つオブジェクトの比較が正しくない
どちらもコーナーケースなので、IE8でもassert/power-assertは十分使えるようになったと言える。
- es6-shim:
Object.keys
のみ対応 (#220)
他のcore.js(6to5)等の大多数のpolyfillは ES5版Objectメソッドの挙動でpolyfillしている 情況。 つまり、polyfillしてるつもりでも実は環境毎に挙動が違うという情況が発生している。
今後はES5版とES6版でpolyfillを区別する必要が出てくる。
ES6版polyfillは、その環境でObject.keys
が存在しても、ES5版の挙動で実装している環境(IE11以下等)に対してはpolyfill実装を適用しなければならない。
例えば以下のようにpolyfillする必要があるはず。
// ES6 polyfill
if (!hasES6ObjectKeys()) {
Object.keys = es6ObjectKeysShim;
}
function hasES6ObjectKeys() {
if (typeof Object.keys !== 'function') {
return false;
}
try {
return isEmptyArray(Object.keys(1));
} catch (ignore) {
return false;
}
}
一方、ES5 polyfillは、ES6版Object.keysが実装されている環境で読まれた場合、ES5版Object.keysで上書きしてダウングレードすべきなのだろうか? ES5を想定したライブラリを動かしたいときはそうしてほしい気がするけど、そんなケースねーよって言われると無いような気もする。
polyfill業界、どうするんだろ。
ちゃんと書いていてもWebは壊れる。
- "Introduing Break the Web: Array extra methods case" by @Constellation at #es6_casual (report by azu)
expect.jsがまだ直ってなかったのでとりあえずissue登録した。