[JavaScript] prototypeに直接代入しちゃうのってダメじゃなかったっけ?

[JavaScript] prototypeに直接代入しちゃうのってダメじゃなかったっけ?

JavaScript基礎文法最速マスター - なんとなく日記という記事がはてブ界隈で話題になっていたので、徒然なるままに読んでみて心に浮かんだことをそこはかとなく書きつけてみる。

まず、どうでもいい細かい点につっこみを入れておくと、「callとapplyは外せないでしょ?」とか「undefinedに限らず、かなりの数の値がオブジェクトではないですよ [注1]」とか「"use strict";してるとhoge();の形で呼び出された場合はthisは window (グローバルオブジェクト)にならないらしいですよ[注2]」とか「for in文ではまりがちな落とし穴って配列走査周りじゃね?」とかいろいろあるんだけど、まあこの辺は別にどうでもいいや。

気になったのは、クラス定義の解説のセクションの↓のコード。

// メソッド・プロパティの定義
Man.prototype = {
  sayName: function() { alert("My name is " + this.name + "."); }
}

確か、コンストラクタのprototypeプロパティに直接オブジェクトを代入してクラスを作るのってあまりよくないんじゃなかったっけ?

あまりお行儀がよくないってことだけ覚えていて、何でいけないのかを思い出せなかったからしばらく悶々としながらコードを書いてみて、とりあえず思い出したから↓に少しまとめておく。

ちょっとしたデメリット: constructorプロパティが変わる

大したデメリットではないけど、コンストラクタのprototypeプロパティにオブジェクトを直接代入すると、インスタンスのconstructorプロパティがコンストラクタを指さなくなる。

// prototypeにプロパティを生やすことでクラス定義
var ConstructorA = function() {};
ConstructorA.prototype.method = function() {};

// instanceAのconstructorはConstructorA
var instanceA = new ConstructorA();
console.log(instanceA.constructor === ConstructorA);  // true

// prototypeにオブジェクトを代入することでクラス定義
var ConstructorB = function() {};
ConstructorB.prototype = { method: function() {} };

// instanceBのconstructorはConstructorBではなくてObject
var instanceB = new ConstructorB();
console.log(instanceB.constructor === ConstructorB);  // false
console.log(instanceB.constructor === Object);        // true

致命的なデメリット: インスタンス化のタイミングによって全然別のインスタンスになる

こっちの方がよほど深刻なデメリット。

prototypeプロパティに直接オブジェクトを代入してしまうと、代入前に生成したインスタンスと代入後に生成したインスタンスとで、全く別のプロトタイプを保持することになってしまって、いろいろと不都合が起こる。

// クラス定義前にインスタンスを作成
var Constructor = function() {};
var instance = new Constructor();

// prototypeにプロパティを生やすことでクラス定義
Constructor.prototype.field = "Prototype A";
Constructor.prototype.method = function() { return this.field; };

// 直接代入しなければインスタンス化後にクラス定義しても平気
console.log(instance.method());  // "Prototype A"

// prototypeにオブジェクトを代入することでクラス定義
Constructor.prototype = {
  field: "Prototype B",
  otherMethod: function() { return this.field; }
};

// 直接代入するともうダメ
console.log(instance.otherMethod());  // TypeError

// instanceは直接代入前のオブジェクトをプロトタイプとして保持している
console.log(instance.method());  // "Prototype A"

「普通はクラス定義する前にインスタンス化することなんてないだろ」とか思うかもしれないけど、JavaScriptの場合関数宣言がトリッキーな挙動をするから間違いが起こらないとは言い切れない。

// クラス定義の前にインスタンス化してしまう例
// JavaScriptの仕様ではA→B→Cの順に実行されるため、
// エラーは起こらないが期待した通りには動かない
(function() {
  var instanceA = new Constructor();          // B
  console.log(instanceA.field);               // undefined

  /* many many lines of code */

  function Constructor() {}                   // A
  Constructor.prototype = { field: "value" }; // C

  var instanceB = new Constructor();
  console.log(instanceB.field);               // "value"
})();

まとめ

クラス定義するときはprototypeプロパティにオブジェクトを代入するのではなくて、prototypeにプロパティを生やしていってクラス定義しましょうって、結構いろんな人が言っていたような気がするけど、検索してみると意外と見つけられない。勘違いだったかな?

いつだかのタイミングで、Prototype.jsの実装が直接代入型からプロパティ生やし型に変わったような記憶があるんだけど、いつだったっけ。

うーん。あまりまとまらないけど、飽きたから終わり。

追記

クラス定義じゃなくて継承の場合はprototypeプロパティに直接代入しなければいけない気がしてきた。Prototype.jsの実装見てるとprototypeプロパティへの直接代入はかなり慎重にやっているように見えるけど、直接代入するかプロパティ生やすかは別に気にしなくてもいいところなのかな?よくわからなくなってきた。

注1

数値型とか文字列型とかブール型とかみたいにtypeof演算子が"object"を返さない値は原始値(native value)と呼ばれるもので、オブジェクトとは扱いがちょっと違う。普段、native valueがオブジェクトに見えるのは、ドット(.)演算子とか角括弧([])演算子が自動的にオブジェクトに変換してくれるから。↓のようなコードを実行してみるとちょっと違うっていうことがわかるかも。

var object = {};
object.property = "property value";
console.log(object.property);           // "property value"

var functionObject = function() {};
functionObject.property = "property value";
console.log(functionObject.property);   // "property value"

var numberValue = 123;
numberValue.property = "property value";
console.log(numberValue.property);      // undefined

var stringValue = "string value";
stringValue.property = "property value";
console.log(stringValue.property);      // undefined

var stringObject = new String("string object");
stringObject.property = "property value";
console.log(stringObject.property);     // "property value"
注2

ECMAScript 5th editionの仕様書の10.4.3 Entering Function Codeのセクションを読むとそんなことが書いてある。this値について以前書いた記事も参照。

スポンサーサイト



関連記事

トラックバック URL

http://liosk.blog103.fc2.com/tb.php/193-b0acfe10

トラックバック

コメント

こんにちは。確かにプリミティブな値については一言入れるべきだったかもです…
ただ、prototypeへの直接代入はそんなに問題ないかなと思います。
constructorの方は、instanceofを使えばOKですし、
「クラス定義の前にインスタンス化してしまう例」
の問題はどちらにしても(そのサンプルコードでは) instanceA.field が undefined という結果は変わりません。
変数に代入するタイミングと参照するタイミングに注意しなければいけないという一般的な話で、prototypeへの直接代入自体の問題ではないように思います。

一応、prototypeへの直接代入が問題となるのは、代入しようとしているコンストラクタのprototypeが何かしらのメソッドやフィールドを持っていた場合に、直接代入ではそれらを消してしまうというケースだったと思います。
  • 2010-02-02
  • by os0x
  • id:LkmvlU/.
  • 編集
該当記事の筆者です。
トラックバックありがとうございます。

プリミティブ値に関しては完全に失念していました...。確かに typeof が object ではないので object ではないですよね。

call, apply は初心者講座なので入れるかどうか迷ったのですが,「thisの指すもの」のところに追記してみました。
確かに JavaScript の特徴的なところなので説明はあった方がよいですね。

prototype に直接代入の件ですが,個人的には問題無いと思っています。
コンストラクタ内から prototype で定義されたメソッドを呼び出している場合ですと,結局どちらのやり方でもエラーになるので,prototype への直接代入よりは,クラス定義が行われる前のコンストラクタ呼び出しを問題視した方が良いと思います。

というわけでos0xさんのコメントに同意です。


余談ですが, prototype への代入がお行儀が悪いというのはこの辺りの議論から来ているのかもしれません。
http://blog.livedoor.jp/dankogai/archives/50808279.html
http://d.hatena.ne.jp/amachang/20070413/1176421425
  • 2010-02-02
  • by gifnksm
  • id:-
瑣末な事項なのに追記していただいて恐縮です。その上、探していたソースまで提示していただけるとは...

確かに、prototypeへの直接代入は本質的な問題ではなさそうですね。os0xさんのいうように、直接代入で既存のメソッドやフィールドを消してしまう以外には深刻な問題はなさそうです。
  • 2010-02-03
  • by LiosK
  • id:-

コメントの投稿

お名前
コメント
編集キー