prototypeとリテラルの、まとまりのないまとめ

リテラルでprototype書いたら - ynsynsのブログの続き。

function Y(){};
Y.prototype={a:'a'};
var y=new Y();

console.log(y.__proto__); // Object { a="a"}
console.log(y.constructor);  // Object()

何故コンストラクタがObjectになるか。

Y.prototype={a:'a'};

これが何をしているかが問題。
リテラル代入は、いきなりプロパティを設定しているのではなく、
いったん空オブジェクトをnewされてからプロパティを入れ直しているということ。

つまり

Y.prototype={}; //いったん空オブジェクトを生成したことになる。なるほどnew Objectと同じ!
Y.prototype.a='a'; //そのあとで、プロパティを設定。

暗黙的にこの2段階の処理がされている。
new Objectされてるのと同義なら、コンストラクタはObjectになるわけだそりゃ。

脱線して、var宣言の例

var x=0; //初期化と同時に値をセットしたい

しかし実際の処理は

var x=undefined; //コンパイル時はスコープのトップレベルでundefinedで定義されたことになる。
    x=0; //続いて値のセット

昔それ周りのこと書いてた。
2010-07-27 - ささいなoutput

話を戻す。
prototypeの場合は厳密にはvar宣言とは異なるかもしれないが、
変数(プロパティ)に値をセットするというのは、
いったん内容が消去(代入されるオブジェクトのコンストラクタに初期化)されてから、新たに書き直されるという動きとなる。
従って、書き直される以前に持っていた属性(適切な言い方ではないかも)が消えてしまう。

パターン実験

function Oya1(){};
Oya1.prototype={a:1}; //リテラルで初期化
var ko1=new Oya1;
console.log(ko1.constructor, ko1.a);

Oya1.prototype={a:2}; //リテラルで更新
console.log(ko1.constructor, ko1.a);


function Oya2(){}
Oya2.prototype.a=1;   //プロパティで初期化
var ko2=new Oya2; 
console.log(ko2.constructor, ko2.a);

Oya2.prototype={a:2}; //リテラルで更新
console.log(ko2.constructor, ko2.a);


function Oya3(){}
Oya3.prototype.a=1;   //プロパティで初期化
var ko3=new Oya3;
console.log(ko3.constructor, ko3.a);

Oya3.prototype.a=2;   //プロパティで更新
console.log(ko3.constructor, ko3.a);


function Oya4(){}
Oya4.prototype={a:1}; //リテラルで初期化
var ko4=new Oya4;
console.log(ko4.constructor, ko4.a);

Oya4.prototype.a=2;   //プロパティで更新
console.log(ko4.constructor, ko4.a);


/*結果
Object() 1
Object() 1

Oya2() 1
Oya2() 1

Oya3() 1
Oya3() 2

Object() 1
Object() 2
*/

Oya3のが最もクリーンなやり方と言える。

constructorの部分について

Oya2,Oya3のように、prototypeのプロパティに設定している場合はconstructorは破壊されず継承している。
Oya1,Oya4のようにリテラル代入した場合は、本来のconstructorが破壊されObjectになる。

プロパティ値を更新した後の挙動

Oya3,Oya4のようにプロパティに変更を与えた時には値が更新される。
Oya1,Oya2のようにリテラルで更新すると、以前の値には影響しない。

Oya2に注目

function Oya2(){}
Oya2.prototype.a=1;   //プロパティで初期化
var ko2=new Oya2; 
console.log(ko2.constructor, ko2.a); // >>>Oya2() 1

Oya2.prototype={a:2}; //リテラルで更新 
console.log(ko2.constructor, ko2.a); // >>>Oya2() 1
// 既に作ったインスタンスに影響しない!

Oya2.prototype.b=1; //追加してみる
console.log(ko2.constructor, ko2.a, ko2.b); // >>>Oya2() 1 undefined
// 追加されない! けどnewされた時点の値は保持している。

var ko20=new Oya2; // 別のインスタンス作る
console.log(ko20.constructor, ko20.a, ko20.b); // >>>Object() 2 1
// constructorが破壊されている!

ko2ではリテラル上書きされた後も、直前の内容を保持している、かつ"追加が不可能"になる。
ko20ではリテラル上書きされたあとのインタンスで、本来のconstructorは破壊され無名Objectで再定義されている。

結論としては、
constructorが破壊されたり、既存のインスタンスへの継承が絶たれたり、
jsのダイナミック性という言語的特徴を否定することにもなるし、
prototypeをリテラルで書くのはやはりよくないという認識に至る。

prototypeの定義は面倒でもプロパティごとでセットするか、applyするかがよろしいかと思う。
意図的に使う高等テクニックもあるのかもしれないけど。


最後に、Oyaとかkoとか芸の無い名前でテストして、途中で残念な気持ちになった。