プロトタイプ汚染とループ
脳内棚卸
Prototype.js と プロトタイプ汚染(昔話)
jQuery が登場する以前、Prototype.js という JavaScript ライブラリがありました。
Prototype.js は、JavaScript OOP の普及期(2005~2007年頃)に多くのサイトで活用されました。
Prototype.js は Object.prototype や Array.prototype 以下に、Ruby 由来のメソッドを拡張することで、
JavaScript に Ruby 感をもたらし、ブームを起こしました。
当時の JavaScript(ES3: ECMAScript262-3rd) には、
言語仕様として Object.prototype を安全に拡張する方法が存在せず、
Prototype.js はしばらく後に 汚染(pollution) と呼ばれる問題を起こしてしまいました。
汚染
Object.prototype.myMethod を拡張すると、
for in ループで myMethod が(意図せず)列挙されてしまいます。これが汚染です。
Object.prototype.myMethod = function() { console.log("myMethod"); }; // 汚染 function eachObject(obj) { for (key in obj) { console.log(key); } // "myMethod", "a", "b" } function eachArray(ary) { for (key in ary) { console.log(key); } // "myMethod", "0", "1", "2" } eachObject({ a: 1, b: 2 }); eachArray([ 1, 2, 3 ]);
現在と違い、2005~2007年当時は JavaScript は表層的にしか理解されておらず*1、
このような挙動は「危険な振る舞い・意図しない結果をもたらすもの」と受け止められました。
このような実装は後に「プロトタイプ汚染(Object.prototype pollution)」や
「オブジェクト汚染(Global Object pollution)」と呼ばれました。
ES5 と プロトタイプ拡張(現在)
Prototype.js の一件以降 Prototype を拡張する行為はプロトタイプ汚染と呼ばれ、禁忌とされていましたが、
最新のJavaScriptの仕様(ES5: ECMAScript262-5th)でオブジェクトを拡張する安全な手段(Object.defineProperty)が追加され、
状況は変化しました。
オブジェクトを拡張する正当な理由と Object.defineProperty による対策を伴うなら、それは拡張です。汚染ではありません。
オブジェクト汚染環境下で for in ループを使う
Object.defineProperty を使わずに拡張(汚染)されている環境であっても、
ユーザが注意深くコードを書くことで汚染の影響を排除できます。
ES3 においては、object.hasOwnProperty(key) を使うことで、
prototype に追加されたプロパティを除外することができます。
Object.prototype.myMethod = function() { console.log("myMethod"); }; // 汚染 function eachObject(obj) { for (key in obj) { if (obj.hasOwnProperty(key)) { console.log(key); } // "a", "b" } } function eachArray(ary) { for (key in ary) { if (ary.hasOwnProperty(key)) { console.log(key); } // "0", "1", "2" } } eachObject({ a: 1, b: 2 }); eachArray([ 1, 2, 3 ]);
# 配列に対して for in ループを行う上記のコードはナイーブです。通常は for (;;) ループを使います。
オブジェクト拡張環境下で for in ループを使う
最新のJavaScript(ES5: ECMAScript262-5th)をサポートしているブラウザでは、
Object.defineProperty を使う事で、オブジェクトを安全に拡張する事ができます。
hasOwnProperty を使い、prototype に追加されたプロパティを除外する必要もなくなります。
Object.defineProperty(Object.prototype, "myMethod", { // 拡張 configurable: true, // false is immutable enumerable: false, // false is invisible writable: true, // false is read-only value: function() { console.log("myMethod"); } }); function eachObject(obj) { for (key in obj) { console.log(key); } // "a", "b" } function eachArray(ary) { for (key in ary) { console.log(key); } // "0", "1", "2" } eachObject({ a: 1, b: 2 }); eachArray([ 1, 2, 3 ]);
オブジェクト汚染/拡張環境下でも使えるループ(uupaa-looper)
ES3, ES5 において、prototype に追加(汚染/拡張)されたプロパティの影響を除外しつつ
ループを高速化する方法に uupaa-looper があります。
for in ループは、ループ開始時に
- オブジェクト以下のプロパティを列挙
- prototype 以下のプロパティを列挙(辿れる限り探索する)
- オーバーライドされているプロパティの解決(重複する key の排除)
といった見えない処理が走ります。
この見えない処理はそれなりにコストを伴います。
一方、Object.keys はオブジェクト以下のプロパティだけを列挙します。prototype 以下は探索しません。
また、hasOwnProperty を使う必要もないため、uupaa-looper は通常の for in ループに比べ、かなり低コストです。
function each(obj) { var key, keys = Object.keys(obj), i = 0, iz = keys.length; for (; i < iz; ++i) { key = keys[i]; console.log( obj[key] ); } }
uupaa-looper は列挙される key 名で予めソートもできる
for in ループで列挙される key の順番は仕様で保証されておらず、実装依存になります。
# for (key in { a:1, b:2, c:3 }) {...} は c, a, b の順番で列挙されるかもしれませんが、それは正しい動作です。
実際には、ほとんどの環境で a, b, c の順番で列挙されますが、
そのような見た目の動作に依存したコードはナイーブなコード(潜在バグ)となります。
uupaa-looper では、Object.keys(obj).sort() とすることで、key名でソートした状態でループを実行できます。
function each(obj) { var key, keys = Object.keys(obj).sort(), i = 0, iz = keys.length; // ~~~~~~~ for (; i < iz; ++i) { key = keys[i]; console.log( obj[key] ); } }
(ε・◇・)з o O ( オバマさんおめでと
*1:JavaScriptとクライアントサイドに未来を感じ取り、取り組んでいる人は、国内において200人にも満たないものでした