JavaScriptでのbuilt-in/DOM objectのprototype拡張
@rosylillyが気にしていた
のでまとめた。built-in/DOM objectのprototype拡張による弊害
追加したプロパティ/メソッドがfor inで列挙される
var obj = {a:1}; for (var i in obj) { console.log(i); }
こうするとaだけ出るはずが、
Object.prototype.b=function(){};
こうした後だとa,bが出てしまうって奴ですね。
そのまま代入しないでObject.defineProperty/definePropertiesでenumerable:falseのプロパティとして定義すれば列挙されなくなるので特に問題ありません。
今回挙げるprototype拡張の弊害の内唯一これだけはECMAScript5時代になって解消されました。唯一これだけは。
built-in/DOM objectの新しい仕様で追加されたプロパティ/メソッドとバッティング
JSどうこうというかどの言語のmonkey patchでも起こりうる問題。
よく見かけた例
- Prototype.js(1.6.0.3以前)でのArray#reduceとECMAScript5のArray#reduce
- Prototype.js の reduce メソッドを使ってはいけない - Qiita
- 導入した時点でバッティングしたならまだいいけど、native Array#reduce登場前からアプリケーションコードでずっと使ってた人は大変だと思う。
- Prototype.js(1.7未満)によるECMAScript5のJSON.stringifyの破壊
- Ooharabucyou «
- 何だか知らんが壊れる
- あまり使わないがJSON.stringifyでJSON文字列化されるobjectのどこかの階層にtoJSONメソッドを持つobjectがあるとその戻り値でその階層が置き換えられる。このtoJSONメソッドの戻り値はプリミティブ or objectであってJSON文字列ではない。一方Prototype.js(1.7未満)ではJSON文字列を返すArray#toJSONを実装していたためJSON.stringifyがまともに機能しなくなっていた。
- 問題が発生するメソッドと拡張された箇所が違うため知らなければ原因特定は困難
JSON.stringify({ b:{ toJSON:function(){return 1;} } }); // '{"b":1}'
ECMAScript6でも便利メソッドは追加されるので今後も似たような状況にはなりうるので、prototype拡張として便利メソッドを実装するのはお勧めしません。
代案としてはやはりラッパーオブジェクト系のライブラリか関数群を提供するライブラリが良いと思います。
ラッパーオブジェクト系のライブラリ
- DOMコレクション
- Array・Object等
- String
- Date
正直Underscore.jsもそろそろラッパーオブジェクト/メソッドチェーンのレイヤと便利メソッドを生やすレイヤを分けてUnderscore.js的なライブラリを作る土壌になったほうが良いのでは?などと。
同じようにbuilt-in/DOM objectのprototype拡張をしている他ライブラリとのバッティング
これもJSどうこうというかどの言語のmonkey patchでも起こりうる問題。
バッティングが起こらないように気を付けろというのは対策になりません。
手動でscriptタグを並べていた時代ならいざしらず今はCommonJS AMD等再帰的に依存関係を辿る真っ当なモジュールシステムがある時代なので、依存関係グラフの途中のどこかとどこかのモジュールでバッティングしているとか目も当てられません。
積極的なmonkey patchが忌避されるのはJavaScriptに限った話ではないということを忘れるべきではありません。
monkey patchでのバッティングの対策は以下のようなものだと思います
- 言語自体で対策を取る
- Ruby2.0で入るらしいClassBox等
- JavaScriptには今の所ありません
- monkey patchを使用している特定のライブラリ・フレームワークが覇権を握ってしまう
- Ruby界隈ではライブラリのmonkey patchがActiveSupportsとバッティングするとバグ扱いになるそうです。
- JavaScriptでのmonkey patch最大手といえばprototype.jsですがActiveSupportsのような扱いはうけていません。
- メソッド・プロパティ名にprefixを付けてバッティングしないようにする
JavaScriptでは上記3対策のうち事実上prefixを付けるくらいしか有効ではないので、便利メソッドを無邪気に生やすのは考えものです。
Re:prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log
prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log
ついでにこのエントリに言及しておくと、このエントリでやってるのは典型的な藁人形メソッドです。
prototype拡張を使わない冗長なコードとprototype拡張を使った自作ライブラリを使った簡潔なコードを比較していますが、冗長なのはprototype拡張を使っていないからではありません。
冗長な使い捨てコードと簡潔なAPIのライブラリを比較しているだけです。
Number#to(prototype拡張あり)とUnderscore.js+ECMAScript5標準(prototype拡張なし)を比較してみましょう。
prototype拡張によって越えた壁はどこかにありますか?
function isX5(n) { // Multiples of 5 filter return n % 5 === 0; } 1..to(100); // -> [1, 2 .. 99, 100] 100..to(1); // -> [100, 99 .. 2, 1] 1..to(100, 2); // -> [1, 3 .. 97, 99] (skip 2) 1..to(100, isX5); // -> [5, 10, .. 95, 100] (filter)
function isX5(n) { // Multiples of 5 filter return n % 5 === 0; } _.range(1,100+1); // [1, 2 .. 99, 100] _.range(1,100+1).reverse(); // [100, 99 .. 2, 1] _.range(1,100+1,2); // [1, 3 .. 97, 99] (skip 2) _.range(1,100+1).filter(isX5); // [5, 10, .. 95, 100] (filter)
built-in/DOM objectのprototype拡張をしてよさそうな箇所
最後にprototype拡張を活用できる・できそうな箇所についても言及しておきます。
shim/pollyfill
標準仕様に入ったAPIを未実装の環境に提供するのは何も問題ありません。バッティングして問題が発生するなら相手のほうが悪いです。どんどんやればいいし、そういうライブラリを提供している人はたくさんいます。
internalプロパティ/メソッド
APIとして公開するものでなくライブラリの中でのみ使うプロパティ/メソッドをまず他とバッティングしないような長い名前で追加するのは問題ありません。もちろんenumerableはfalseにする。例えばGitHub - monjudoh/BeautifulProperties.jsでObject.prototypeに対してEvents.onしたら'BeautifulProperties::internalObjectKey'というkeyのプロパティが追加されるがこれはバッティングしないでしょう。
小規模なWebWorker内
この場合DOM objectのprototype拡張はそもそもありません。Worker間・Worker/window間でbuilt-in objectのprototypeは共有されないので、Worker内での拡張が他に影響することはありません。個々のWorkerのscriptのサイズをprototype拡張の影響を充分に追いきれる規模に止めれば良いでしょう。