Edited at

Object.prototypeの拡張≒グローバル関数の定義

More than 5 years have passed since last update.

JavaScriptは標準でprototypeを使ったオブジェクト指向をサポートしているわけで(これについてはここらへんを参考に)、それを利用することでビルトインのオブジェクト(NumberArrayなど)を拡張することができました。が、このビルトインオブジェクトの拡張は長い間禁忌とされてきました。その理由はいくつかありますが、代表的な理由としては、「for( .. in .. )文でオブジェクトを列挙しようとした時に拡張したものまで列挙されてしまう」ことや「複数のライブラリでビルトインオブジェクトを拡張したとき、名前が衝突することがる」ことなどが挙げられます。というかだいたいこの二つです。この二つのうち、前者はES5でproperty descriptorをいじれるようになって、列挙するかどうかを制御することが出来るようになったため問題では無いのですが、最後のは現状どうしようもありません(Ruby 2.0のRefinementsみたいな仕様ができない限り)。でもまあ、外部に公開するライブラリじゃないなら自己責任でプロトタイプ拡張ぐらい行なってもいいんじゃないかって思ってます。

が、Object.prototypeを拡張するのは絶対にだめです。

薬物乱用並にダメ絶対です。

これからその理由を書いて行きたいと思います。


Object.prototypeを拡張するとどうなるの?

まずはObject.prototypeを拡張する例を。

function extendMethod(object, methodName, method) {

if(typeof Object.defineProperty !== 'function') {
object[methodName] = method;
} else {
Object.defineProperty(object, methodName, {
configurable: false,
enumerable: false,
value: method,
});
}
}

extendMethod(Object.prototype, 'forEach', function(iter) {
var
key;
for(key in this) {
iter.call(this, this[key], key, this);
}
});

extendMethodというプロトタイプを拡張するための関数を定義して、それを使ってObject.prototypeforEachというメソッドを追加しています。

わざわざextendMethodなんて関数を作ったのは、Object.definePropertyのない環境でも動くようにするためと、Object.definePropertyの設定値を統一させるためです。

こうすると、こんなことができます。

!{

foo: 1,
bar: 2,
baz: 3,
}.forEach(function(val, key) {
console.log('%s : %s', key, val);
//=> foo : 1
//=> bar : 2
//=> baz : 3
});

ここで重要なのはforEachという本来はなかったメソッドを呼び出すことができている点です。

これだけだと便利になっているような気がします。ががが、これには大きな問題があります。


Object.prototypeを拡張したときの弊害

Object.prototype.forEachが拡張された状態でこのコードを試してみましょう。

''.forEach(function(val, key) {

console.log('%s : %s', key, val);
});

1..forEach(function(val, key) {
console.log('%s : %s', key, val);
});

true.forEach(function(val, key) {
console.log('%s : %s', key, val);
});

!function(){}.forEach(function(val, key) {
console.log('%s : %s', key, val);
});

一見訳の分からないコードですが、これは正常(≒例外を吐くこと無く)に動きます。動くことがなにか問題なの? と思うかもしれませんが、これって結構大問題です。無意識に全てのオブジェクトにforEachメソッドを追加してしまったわけですから。

これは、全てのオブジェクトの基底クラスであるObjectprototypeを変更したために他の全てのprototypeも変更してしまったからです。Javaでいうと、java.lang.Objectクラスにメソッドを追加するようなものです。

(ちなみにArrayの場合はforEachが自身のプロトタイプで宣言されているので影響ありません)

ですが、この程度なら別にいいんじゃない? というような気もします。_.each(obj,...)とか書くよりobj.forEach(...)と書いたほうがメソッド呼び出しらしくて分かりやすいですし。

では、次のセクション。


Object.prototypeの拡張≒グローバル関数の定義

この記事のタイトルです。

どういうことなのかというと、JavaScriptのグローバルオブジェクトはObjectを継承しているので、グローバルスコープからさっき定義したforEachを呼び出すことができます。

つまり、

forEach(function(val, key) {

console.log('%s : %s', key, val);
});

という呼び出しが可能になってしまいます。(上のコードはNode.jsだと意外とたくさん出力するので注意してください)

(仕様ではグローバルオブジェクトがObjectを継承するとは限らないんだけど、V8もSpiderMonkeyも継承しているのであまり関係ないのです。昔のIEは継承してなかった気がするけどあまり関係ないのです)

一般にグローバル変数とか関数はヤバイと言われています。というのも、プログラムのどこからも参照できるため、タイポがあったりすると予期せぬ挙動を見せたりします。

例えば、ローカルなスコープで少し特殊にオブジェクトを列挙するforEachという関数を定義しようとしたのだけどタイポしてforEAchという名前になってしまったとする。その状態でforEachを呼び出したらObject.prototype.forEachが呼ばれて……あとは分かるな?

まあforEach程度なら大した問題にはなりませんが、これが運命のめぐり合わせで引数が一致したりするとかなり発見しにくいバグに化けたりします。怖いです。


最後に

例をコードで示すと無駄に長くなってしまって本質的なところが見えにくかったので全て言葉で示したのですが、わかりにくくなってしまってすみません。

あと最初に、Object.prototypeを拡張するのは薬物並にダメ絶対と書きましたが、薬物並にスリリングな感覚を楽しみたいならやってみてもいいかもしれません。しかし、自己責任です。やったあとに激しい喪失感に襲われても僕は知りません。

それでは、ありがとうございました。