[javascript メモ] コンストラクタ関数をnewした時の状態のメモ

A.
JavaScriptは、コンストラクタ用に用意した関数をnewすると
オブジェクトのインスタンスを作ることが可能です。

newを行うとコンストラクタ関数の内のthisにインスタンス自身が
代入されるので、thisのプロパティに対して変数を設定する事で
インスタンス変数を作成する事ができます。

同じようにthisのプロパティに対して関数を代入するとインスタンス
メソッドの作成が行えます。

  var Person = function(nickname){
    this.nickname = nickname;
  };
  Person.prototype.getName = function(){
    return this.nickname;
  }
  var per = new Person('nicnic');


  // プロトタイプが何を示しているのか
  console.log('#プロトタイプ');
  console.log(per.__proto__ === Person.prototype); // true
  console.log(per.__proto__.__proto__ === Object.prototype); // true

  // コンストラクタが何を示しているのか
  console.log('#コンストラクタ'); 
  console.log(per.constructor === Person); // true
  console.log(per.__proto__.constructor === Person); // true
  console.log(per.__proto__.__proto__.constructor === Object); // true

  // プロトタイプ
  console.log(per.__proto__);
  console.log(per.__proto__.__proto__);
  console.log(per.__proto__.__proto__.__proto__); // null

 // ログ
  console.log('#ログ');
  console.dir(per);
  console.dir(per.getName());


上記のログから以下の図のようになっているのが確認できる
js_new_log_1.png

js_new_log_2.png

インスタンスメソッドを追加したい
コンストラクタでは、thisがインスタンスになるので
プロパティにメソッドを追加するとインスタンスメソッドを
設定する事が可能です。

  var Person = function(nickname){
    this.nickname = nickname;
    this.getName = function(){
      return this.nickname;
    }
  };
  var per1 = new Person('nic');
  var per2 = new Person('yaki');
  var per3 = new Person('star');

インスタンスを作成すると作成毎にプロパティ、
メソッドがコピーされてメモリ上に確保されます。
js_new_log_3.png

プロパティはそれぞれインスタンス毎の値を持つ必要がありますが
メソッドにおける処理はどのインスタンスでも同じなので
インスタンス作成毎にコピーして複製をつくるのはメモリ資源
の無駄使いになってしまいます。

この問題を解決するには、コンストラクタのthisにメソッドを設定
するのではなく、コンストラクタ関数.prototypeプロパティに
メソッドを追加します。

prototypeプロパティに追加したメンバは、
生成されたインスタンスから暗黙の参照されメンバの利用が可能になります。
  var Person = function(nickname){
    this.nickname = nickname;
  };
  Person.prototype.getName = function(){
    return this.nickname;
  }
  var per1 = new Person('nic');
  var per2 = new Person('yaki');
  var per3 = new Person('star');

インスタンスに指定したメンバが存在しない場合、
prototypeをたどりprototypeに指定したメンバが存在するか確認します。
js_new_log_4.png

プロトタイプの動的な特性
また、インスタンスからプロトタイプへは参照なので
プロトタイプのメンバ情報の変更はリアルタイムに
インスタンスに反映されます。

  var Person = function(nickname){
    this.nickname = nickname;
  };

  Person.prototype.getName = function(){
    return this.nickname;
  };

  var per = new Person('nic');
  var per2 = new Person('yaki');

  // メソッドをインスタンス生成後に作成
  Person.prototype.getNameHello = function(){
    return "Hello! " + this.nickname;
  };

  console.log( per.getNameHello() );
  console.log( per2.getNameHello() );

js_new_log_5.png

プロトタイプにプロパティを設定して上書きすると?
プロトタイプにプロパティを設定した場合はどうでしょう?
プロパティはインスタンスでも読み込み可能です。
インスタンスでプロパティの値を変更した場合は
プロトタイプにプロパティの変更にはならず、
インスタンスに新規にプロパティが生成されます。

プロトタイプにプロパティを設定し値を変更して確認してみます。
var Person = function(){};

Person.prototype.getName = function(){
  return this.nickname;
};

// prototypeにプロパティを設定
Person.prototype.nickname = "mike";

// インスタンスを作成
var per = new Person('nic');
var per2 = new Person('nic');

// nicknameを上書き
per.nickname = "taro";

console.log( per.getName() );
console.log( per2.getName() );

console.log( per );
console.log( per2 );

この時のコンソールログは以下のようになります
js_prototype_proto_2.png

ログの結果からnicknameに値を代入するとインスタンスのプロパティに
新規に値が作成されprototypeの値は変更されない事が確認できます。

従ってプロトタイプチェーンによってプロトタイプのプロパティを検索する前に
インスタンスのプロパティとして先に検索されるので、
nicknameに値を代入するとそれぞれ別の値を持つ事になります。
この事を図に表すと以下のようになります。
プロトタイプにプロパティを設定し上書した時の図
js_prototype_proto_1.png

関数オブジェクトのprototypeプロパティとは?
関数オブジェクトはprototypeプロパティをもっています。
関数オブジェクトがコンストラクタとして呼び出された場合
インスタンスの内部__prototype__の初期化に利用されます。

Functionオブジェクトを確認する
var Person = function(){};
console.log( typeof Person );
console.log( typeof Person.prototype );
console.dir( Person );
関数オブジェクトのprototypeプロパティ
js_log_n1.png

ECMA-262 3rd Edition翻訳 15.3 Function オブジェクト prototypeにこの仕様が定義されています。

実際にコンストラクタのプロパテ prototype と作られたオブジェクトである
インスタンスの内部プロパティ[[prototype]](__proto__)を比較してみます。
var Person = function(nickname){
  this.nickname = nickname;
};

// メソッドの作成
Person.prototype.getName = function(){
  return this.nickname;
}

// プロパティの作成
Person.prototype.myname = 'myname';

// インスタンスの作成
var per = new Person('nicnic');

// インスタンスの内部プロトタイプ[[prototype]]
console.log( per.__proto__);

// コンストラクタのプロパティ prototype
console.log( Person.prototype );

// コンストラクタのprototypeとインスタンスの__proto__を比較
console.log( per.__proto__ === Person.prototype ); // true
js_prototype_proto_3.png
コンストラクタのprototypeとインスタンスの__proto__は同じになる事が確認できました。
これによりコンストラクタのprototypeにメンバーを追加
した際に動的に反映される事しくみが理解できます。



ここからは調査メモ(未検証で不正確かもしれません)
newで何が行われているかを確認してみる。
いままでで確認したのは以下の事項である
1.新しいObjectを作成する
2.内部の[[prototype]]にコンストラクタのprototypeを代入する
3.コンストラクタを新しいObjectのコンテキストで実行する
実際試してみる
var Person = function(nickname){
  this.nickname = nickname;
};

// メソッドの作成
Person.prototype.getName = function(){
  return this.nickname;
}

// インスタンスの作成
var per = new Person('nicnic');

// newっぽい動作を真似してみる
var per2 = {};
per2.__proto__ = Person.prototype;
Person.apply(per2, ['nicnic']);

// ログ
console.log(per);
console.log(per2);

console.log(per.getName());
console.log(per2.getName());
js_prototype_proto_4.png 動作は正しそうですが、これについては正しいかは未検証です。
chromeのconsole.logのオブジェクトの名前はどこから解決してるのだろう?などは課題です。

ECMA-262 3rd Editionの仕様は、JavaScript の new 演算子の意味が参考になります。

ECMAScript5のObject.createでは第一引数に渡したオブジェクトを
プロトタイプとし、新しいオブジェクトを作ります。
従って以下のように書き換えが可能です。
var Person = function(nickname){
  this.nickname = nickname;
};

Person.prototype.getName = function(){
  return this.nickname;
}

var per = Object.create(Person.prototype);
Person.apply(per, ['nicnic']);

console.log(per);
console.log(per.getName());
js_create_cp_1.png
誤ってコンストラクタの呼び出しにnewを付け忘れた場合、thisの値が
グローバルオブジェクトになってしまうので、インスタンスプロパティのはずが
グローバルオブジェクトのプロパティに上書してしまうかもしれないので、注意が必要です。

参考サイト:
ECMA-262 3rd Edition翻訳 15.3 Function オブジェクト