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

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

JavaScriptの少し独特なオブジェクト指向についてまとめてみた

Javascript まとめ

f:id:nipe880324:20150311105033j:plain
Photo by Flickr: slworking2's Photostream

既にJavaやPHPなどオブジェクト指向言語を生業としてやっているが、その感覚でJavaScriptを少し扱っているて、いまいちJavaScript分からないという方を対象に、それらのプログラム言語とは違う、「少し独特なJavaScriptのオブジェクト指向」について説明します。

目次

  1. オブジェクトの作成
  2. プロパティの定義と代入
  3. プロパティの削除
  4. アクセサプロパティ(getter / setter)
  5. クラス定義
  6. コンストラクタ引数
  7. インスタンスメソッドの定義
  8. クラスプロパティとクラスメソッドの定義
  9. クラスの継承
  10. クラスプロパティとクラスメソッドの継承

1. オブジェクトの作成

大かっこ({})でObjectクラスを作成することができます。
また、new演算子で特定のオブジェクトを作成することができます。

// 作成方法1({}でObjectインスタンスを作成する)
var obj = {};

// 作成方法2(new演算子でインスタンスを作成する)
var obj = new Objcet();    // Objcetインスタンス
var person = new Person(); // Personインスタンス


2. プロパティの定義と代入

オブジェクトにプロパティに値を設定すると、プロパティ定義と値の設定ができます。

var obj = {};

// プロパティが定義されていないとundefinedが返される
console.log(obj.prop); // => undefined

// プロパティに値を設定することで、プロパティ定義も一緒に行える
obj.prop = 1;
console.log(obj.prop); // => 1

オブジェクトの作成時に、プロパティを定義、設定することもできます。

// プロパティを定義、設定することができる
var obj2 = {
  prop:  1,
  prop2: 'foo'
};

console.log(obj2.prop);  // => 1
console.log(obj2.prop2); // => 'foo'


3. プロパティの削除

delete演算子によって、プロパティを削除することができます。
削除後にプロパティにアクセスすると、プロパティが定義されていないと同じようにundefinedを返します。

var obj = {};
obj.prop = 1;

// プロパティの作成
delete obj.prop;

// プロパティを参照すると、undefinedを返す
console.log(obj.prop); // => undefined

プロパティにイコールでundefinedを設定することで、プロパティの削除をしないでください。
その場合、forループを使用してプロパティを一覧すると、undefinedを設定したプロパティも参照されてしまいます。
そのため、プロパティの削除は、delete演算子を使ってください。



4. アクセサプロパティ(getter / setter)

getsetを使うことでアクセサプロパティを定義することができます。

var circle = {
  redius : 1,    // 半径
  get diameter()      { return this.radius * 2; }, // 直径は半径から算出
  set diameter(value) { this.radius = value / 2; } // 直径から半径を算出
};

circle.diameter = 5;          // set diameter が呼ばれる
console.log(circle.radius);   // => 2.5
console.log(circle.diameter); // => 5 (get diameterが呼ばれる)


5. クラス定義

JavaScriptのクラス定義は、コンストラクタ内でプロパティを定義することで行います。

// Personクラス(コンストラクタ)の定義
var Person = function(name, age) {
  this.name = name;
  this.age  = age;
}

// Personインスタンスの作成
var satoshi = new Person('サトシ', 28);
console.log(satoshi.name); // => 'サトシ'
console.log(satoshi.age);  // => 28
console.log(satoshi instanceof Person) // => true (satoshiはPersonクラスのインスタンス)


6. コンストラクタ引数

JavaScriptでは、関数呼び出し時に引数が省略できます。コンストラクタも関数であるため、コンストラクタ引数も省略できます。
つまり、引数が省略されてコンストラクタが呼び出されてインスタンスが作成されると、プロパティが未定義(undefined)のままになってしまい、予期せぬエラーが発生する可能性がでてきてしまいます。

そのため、対処法として、

  • プロパティを特定の値で初期化する
  • エラーを発生させプログラムを中断する

という方法が考えられます。

プロパティを特定の値で初期化する

var Person = function(name, age) {
  // 特定の値で初期化する
  this.name = name || 'No name';
  this.age  = age  || 20;
}

// コンストラクタ引数を指定しないと「特定の値」で初期化される
var satoshi = new Person();
console.log(satoshi.name); // => 'No name'
console.log(satoshi.age);  // => 20

// コンストラクタ引数を渡すと「渡した値」で初期化される
var satoshi = new Person('サトシ', 28);
console.log(satoshi.name); // => 'サトシ'
console.log(satoshi.age);  // => 28

エラーを発生させプログラムを中断する

var Person = function(name, age) {
  // 引数をチェックする
  if (name == undefined) { throw new Error("引数'name'を指定してください。"); }
  if (age == undefined)  { throw new Error("引数'age'を指定してください。"); }

  this.name = name;
  this.age  = age;
}

// コンストラクタ引数を指定しないとエラーが発生する
var satoshi = new Person(); // => Error: 引数'name'を指定してください。


7. インスタンスメソッドの定義

JavaScriptでオブジェクトにメソッドを定義する方法には次の2つがあります。

  • functionをプロパティに代入する
  • prototypeという特殊なオブジェクトを利用する

functionをプロパティに代入する

var Person = function(name, age) {
  this.name = name;
  this.age  = age;

  // functionをプロパティに代入する
  this.greet = function() {
    console.log('Hello, ' + this.name);
  }
}

var satoshi = new Person('サトシ', 20);

// メソッドとして呼ぶことができる
satoshi.greet(); // => 'Hello, サトシ'

この方法の問題点は、new Person(...)でインスタンスを作るたびに、greet()メソッドもその数だけ作成されてしまいます。そのため、インスタンスを作るたびにメモリを無駄に消費してしまいます。
この解決方として、次の「prototypeという特殊なオブジェクトを利用する」方法があります。

prototypeという特殊なオブジェクトを利用する

var Person = function(name, age) {
  this.name = name;
  this.age  = age;
}

// すべてのクラスが持つ特別なprototypeオブジェクトにメソッドを代入する
Person.prototype.greet = function() {
  console.log('Hello, ' + this.name);
}

var satoshi = new Person('サトシ', 20);

// メソッドとして呼ぶことができる
satoshi.greet(); // => 'Hello, サトシ'


こうすることで、すべてのPersonインスタンスは、Person.prototype.greet()を参照するので、インスタンスを作成するたびに無駄なメモリ消費をしなくなります。

JavaScriptの処理の内部処理の順序は、greet()メソッドが呼ばれると、まずPersonインスタンス内に定義されたプロパティを検索し、見つからないのでPerson.prototype内を検索し、見つかったのでそのgreet()メソッドを実行しています。




8. クラスプロパティとクラスメソッドの定義

JavaScriptのクラスメソッドは、クラスプロパティの定義と同じように定義できます。

var Person = function(name, age) {
  this.name = name;
  this.age  = age;
}

// クラスプロパティ(定数)の定義
// 何歳以下をyoung(若い)とするかを保持する定数
Person.YOUNG_LIMIT_AGE = 20;

// クラスメソッドを定義
// youngの場合 true, そうでない場合 false を返す
Person.isYoung = function(age) {
  if (age <= Person.YOUNG_LIMIT_AGE) {
    return true;
  }
  return false;
}

// クラスプロパティを呼び出す
console.log(Person.YOUNG_LIMIT_AGE); // => 20

// クラスメソッドを呼び出す
console.log(Person.isYoung(10)); // => true
console.log(Person.isYoung(30)); // => false

定数は慣例で大文字で定義していますが、Rubyのように変更不可能になるという言語仕様はありません。
あくまで大文字にすることで定数として変更してはいけないという慣例があるだけで、変更は可能です。(変更しないでください!)



9. クラスの継承

JavaScriptでは、継承のための構文は用意されていません。次のような方法で継承を行います。

  • プロパティの継承は、applyメソッドを呼び出す
  • メソッドの継承は、prototypeオブジェクトを利用する

JavaScriptでのクラス継承はなかなか複雑なので、小規模の場合は、継承を使わないようにし、大規模の場合は、ライブラリなどを使い継承を行う方が良いらしいです。

Webや本では細かな違いがいろいろあり、下記の継承のコードは妥当でない可能性があります。

プロパティの継承は、applyメソッドを呼び出す

サブクラスのコンストラクタ内でスーパークラスのapplyメソッドを呼ぶことで、スーパークラスのプロパティをサブクラスに引き継ぎます。

// Personクラス
var Person = function(name, age) {
  this.name = name;
  this.age  = age;
}

// Personを継承したEmployeeクラス
var Employee = function(name, age, jobTitle) {
  this.jobTitle = jobTitle;

  // スーパークラス(Person)のコンストラクタを呼び出す
  Person.apply(this, [name, age]);
}

var satoshi = new Employee('サトシ', 28, 'QA');
console.log(satoshi.name);     // => 'サトシ'
console.log(satoshi.age);      // => 28
console.log(satoshi.jobTitle); // => 'QA'
console.log(satoshi instanceof Employee); // => true

メソッドの継承は、prototypeオブジェクトを利用する

prototypeオブジェクトのプロトタイプチェーンの仕組みを利用して継承を行います。

// Personクラスを定義
var Person = function(name, age) {
  this.name = name;
  this.age  = age;
}

// Person#greet()メソッドを定義
Person.prototype.greet = function() {
  console.log('Hello, ' + this.name);
}

// Personを継承したEmployeeクラスを定義
var Employee = function(name, age, jobTitle) {
  this.jobTitle = jobTitle;

  // Personのプロパティを継承
  Person.apply(this, [name, age]);
}

// プロトタイプチェーンにPersonインスタンスを設定することで、
// Employeeインスタンスのメソッド呼び出しでは、Person.prototypeのメソッド内にメソッドがあるか検索されます。
Employee.prototype = Object.create(Person.prototype);

// 一行上のコードでconstructorプロパティがPersonになってしまうので、Employeeをセットします。
Employee.prototype.constructor = Employee;

var satoshi = new Employee('サトシ', 28, 'QA');
satoshi.greet();                  // => 'Hello, サトシ'
console.log(satoshi.constructor); // => Employeeのコンストラクタ( function(name, age, jobTitle) { ... } )


10. クラスプロパティとクラスメソッドの継承

JavaScriptではクラスプロパティやクラスメソッドの継承は一般的には行われません。
これは、JavaScriptではスーパークラスのクラスプロパティやクラスメソッドをサブクラスで利用するためには、それらのプロパティ定義をサブクラスにもコピーしなければならないので、コードが重複してしまうのでよくないためです。


以上です。

参考文献

  • 独習JavaScript
  • メンテナブルJavaScript