JavaScriptは標準でprototype
を使ったオブジェクト指向をサポートしているわけで(これについてはここらへんを参考に)、それを利用することでビルトインのオブジェクト(Number
、Array
など)を拡張することができました。が、このビルトインオブジェクトの拡張は長い間禁忌とされてきました。その理由はいくつかありますが、代表的な理由としては、「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.prototype
にforEach
というメソッドを追加しています。
わざわざ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
メソッドを追加してしまったわけですから。
これは、全てのオブジェクトの基底クラスであるObject
のprototype
を変更したために他の全ての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
を拡張するのは薬物並にダメ絶対と書きましたが、薬物並にスリリングな感覚を楽しみたいならやってみてもいいかもしれません。しかし、自己責任です。やったあとに激しい喪失感に襲われても僕は知りません。
それでは、ありがとうございました。