latest log

酩酊状態で書いたエンジニアポエムです。酩酊状態で読んでください。

プロトタイプ汚染とループ

脳内棚卸

Prototype.js と プロトタイプ汚染(昔話)

jQuery が登場する以前、Prototype.js という JavaScript ライブラリがありました。
Prototype.js は、JavaScript OOP の普及期(2005~2007年頃)に多くのサイトで活用されました。

Prototype.js は Object.prototype や Array.prototype 以下に、Ruby 由来のメソッドを拡張することで、
JavaScriptRuby 感をもたらし、ブームを起こしました。

当時の 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人にも満たないものでした