読者です 読者をやめる 読者になる 読者になる

頭の整理

JavaScript, Ruby, RSpec, Node.js, Rails TDDなどに興味があるWeb系SEが学んだことを整理していきます

【JavaScript】プロトタイプベースのオブジェクト指向について

JavaScript オブジェクト指向 プロトタイプベース 継承

※Object.createの使い方に関して修正しました.
プロトタイプベースのオブジェクト指向と,JavaScriptでのその扱い方について混乱していたので整理しました.コンストラクタ,プロトタイプ,インスタンスの用語の整理とprototypeプロパティの説明,継承の実現方法などについてサンプルコードを含めて書きました.

コンストラクタ,プロトタイプ,インスタンス
var str = new String;
str.slice; // => function
str.slice();

コンストラクタ(JavaScriptではFunctionオブジェクト)はオブジェクトを生成するもの(上記コードのString)です.コンストラクタから生成されたオブジェクトをインスタンスと呼びます(上記コードのstr).

コンストラクタはプロトタイプを保持します.プロトタイプはオブジェクトです.コンストラクタから生成したインスタンスはプロトタイプにある関数を使用することができます.
例えば,上記コードではstrが保持しているプロパティにsliceがない場合はコンストラクタStringのプロトタイプにあるsliceプロパティの値を返します.

3行目の処理の順序は以下のような流れです.

  1. strオブジェクトのsliceプロパティを参照する
  2. Stringのプロトタイプにあるslice(Functionのオブジェクト)を返す
  3. 返されたsliceを実行する
プロトタイプチェーン

プロトタイプはプロパティを参照するときにクラス継承のルートに向かって探索していきます.
例えば,StringはObjectを継承しているため,Objectコンストラクタのプロトタイプを参照できます.

var str = new String;
str.toString; //=> function
サンプルコード

クラスの定義と継承をコードで表現すると以下のようになります.
これ以降の節で説明していきます.

(function () {
  var profile = {
        age: {value: 26},
        favorite_food: {value: 'orange'}
      }
    , aMan
  ;

  function Human () {}
  Human.prototype.eat = function () {
    console.log('yummy ' + this.favorite_food + '!');
  }

  function Man () {}

  Man.prototype = Object.create(Human.prototype);
  Man.prototype.dance = function () {
    console.log('put your hands up!');
  }

  aMan = Object.create(Man.prototype, profile);

 //プロパティの確認
  console.log(aMan.age);
  
  //constructor関数 [Function: Man]にはならない
  console.log(aMan.constructor); //=> [Function: Human]

  //関数実行の確認
  aMan.eat();
  aMan.dance();

  console.log(Man.eat);
  console.log(Man.prototype.eat);
}());
prototypeプロパティ

Functionオブジェクトはprototypeというプロパティを持っています.これが冒頭で説明したプロトタイプであり,実体はObjectオブジェクトです.prototypeのプロパティに設定した関数はインスタンスから参照可能です.
サンプルコードでは下記のように関数宣言の後に,Humanのプロトタイプにeat関数を,Manのプロトタイプにdance関数を追加しています.

  Human.prototype.eat = function () {
    console.log('yummy ' + this.favorite_food + '!');
  }
  Man.prototype.dance = function () {
    console.log('put your hands up!');
  }

コンストラクタはObject.createまたはnew式を適用してインスタンスを生成します.
サンプルコードでは下記のように使用されています.

  aMan = Object.create(Man.prototype, profile);

Functionでないオブジェクトにはprototypeプロパティはありませんが,内部プロパティの[[Prototype]]はすべてのオブジェクトが持っています.
コンストラクタから生成されたインスタンスは内部プロパティ[[Prototype]]によってコンストラクタのprototypeへの参照を実現しています.

状態の保持

コンストラクタはオブジェクトを生成しますが,生成するオブジェクトはthisという名前でコンストラクタ関数本体から参照できます.
また,thisにプロパティを設定することで生成したインスタンスのプロパティとして状態を保持できます.
Object.createの第二引数に下記のような形のオブジェクトを渡すことで,生成するインスタンスのage, favorite_foodプロパティに値をセットできます.

  var profile = {
        age: {value: 26},
        favorite_food: {value: 'orange'}
      }

関数内で宣言された変数はそのスコープのみで参照可能となるため,コンストラクタ内で宣言された変数は生成したオブジェクト内では参照できません.

function MyScope () {
  var pri = 'private';
  this.pub = 'public';
}

var scope = new MyScope;
scope.pub; //=> 'public'
scope.pri; //=> undefined
new演算子を付け忘れた場合
function Constructor () {
  this.hoge = 1;
}
var hoge = Constructor();

上記の場合,関数はグローバルオブジェクトを起点として実行されるため.
上記のhogeはグローバルオブジェクトにセットされるため,意図しない値がグローバルに確保されることになります.

継承

サンプルコードではManがHumanを継承していることを表現しています.

  function Human () {}
  Human.prototype.eat = function () {
    console.log('yummy ' + this.favorite_food + '!');
  }

  function Man () {}

  Man.prototype = Object.create(Human.prototype);
  Man.prototype.dance = function () {
    console.log('put your hands up!');
  }

  aMan = Object.create(Man.prototype, profile);

下記のようにManインスタンスはHumanのプロトタイプがもつ関数とManのプロトタイプがもつ関数両方を実行できます.

  aMan.eat(); //=> yummy orrange!
  aMan.dance(); //=> put your hands up!
JavaScriptにおけるクラス

JavaScriptにはクラス定義構文は(ES5には)ありませんが,分類としてのクラスは実現されています.
ですが,同じクラスであるということに対する考え方がJavaScriptとその他のプロトタイプベースでない言語とは異なっています.

Javaなどのクラスベースのオブジェクト指向ではおなじAPI群を持っているものを同一のクラスとみなしますが,JavaScriptなどのプロトタイプベース(インスタンスベース)のオブジェクト指向では,同一のプロトタイプを参照するかどうかによって同一のクラスとみなします.
これによってクラスベースでは1つのインスタンスのために新たにクラスを定義する必要がありますが,JavaScriptなどのプロトタイプベースではそのインスタンスのみに処理を追加すればよいという考え方の違いがあります.