JavaScript 入門

JS オブジェクトコンストラクタとプロトタイプ

これまでの章では、オブジェクトリテラル(例:{ name: "Alice", age: 30 })を使用して単一のオブジェクトを作成する方法を学びました。これは、1つまたは数個のユニークなオブジェクトが必要な場合には完璧な方法です。

しかし、従業員リスト、製品カタログ、あるいはゲームのキャラクターセットなど、数百から数千もの類似したエンティティを管理する必要があるアプリケーションを構築していると想像してみてください。一つひとつ手動でオブジェクトを作成していては、作業は極めて冗長になり、ミスが発生しやすく、メンテナンスも困難になります。

もし、これらすべてのオブジェクトが同じプロパティセット(例:name, age)と同じ振る舞い(例:introduce, work)を必要とするなら、どうすればよいでしょうか?ここで JavaScript における オブジェクトコンストラクタ (Object Constructors)プロパティ (Prototypes) が極めて重要になります。これらを使用することで、オブジェクトの「設計図(ブループリント)」を作成し、機能を効率的に共有できるようになります。

1. オブジェクトコンストラクタの理解

オブジェクトコンストラクタ は、似たようなプロパティとメソッドを持つ複数のオブジェクトを作成するための「設計図」として機能する特別な関数です。

これを「クッキーの型」に例えることができます。一つの型を使って、同じ形をした多くのクッキーを作ります。JavaScript では、new キーワードをコンストラクタ関数と組み合わせて使用することで、その設計図に基づいた新しい インスタンス (Instance)(オブジェクト)を作成します。

1.1 コンストラクタ関数の作成

コンストラクタ関数は本質的に通常の JavaScript 関数ですが、慣習として 最初の文字を大文字にする というルールがあります。これにより、通常の関数と区別します。

コンストラクタ関数の内部では、this キーワードは新しく作成されるオブジェクト(設計図のインスタンス)を指します。属性やメソッドを this に割り当てることで、オブジェクトの特徴と振る舞いを定義します。

例を見てみましょう:

// 'Person' という名前のシンプルなコンストラクタ関数
function Person(name, age) {
  this.name = name; // 'this.name' は新しい Person オブジェクトの 'name' プロパティを指す
  this.age = age;   // 'this.age' は新しい Person オブジェクトの 'age' プロパティを指す
    
  // コンストラクタ内部で直接定義されたメソッド
  // 後ほど、より効率的なメソッドの扱い方を説明します
  this.greet = function() {
    console.log(`こんにちは、私の名前は ${this.name} です。今年で ${this.age} 歳になります。`);
  };
}

この Person コンストラクタにおいて:

  • function Person(...) として定義されています。
  • nameage を引数として受け取り、これらを使用して新しいオブジェクトのプロパティを初期化します。
  • this.name = name; は、渡された値を生成中のオブジェクトの name プロパティに代入します。
  • this.greet = function() { ... }; は、このコンストラクタで作成される各オブジェクト固有の greet メソッドを定義します。

1.2 new によるオブジェクトの生成

コンストラクタからオブジェクト(インスタンス)を作成するには、new キーワードに続けてコンストラクタを呼び出します。

// コンストラクタを使用して 2 つの新しい Person オブジェクトを作成
const person1 = new Person("アリス", 30);
const person2 = new Person("ボブ", 25);

// プロパティへのアクセス
console.log(person1.name); // 出力: アリス
console.log(person2.age);  // 出力: 25

// メソッドの呼び出し
person1.greet(); // 出力: こんにちは、私の名前は アリス です。今年で 30 歳になります。
person2.greet(); // 出力: こんにちは、私の名前は ボブ です。今年で 25 歳になります。

new キーワードを使用した際、舞台裏では以下のことが起こっています:

  1. 新しい空の JavaScript オブジェクトが作成される。
  2. コンストラクタ内部の this キーワードが、自動的にこの新オブジェクトにバインド(紐付け)される。
  3. コンストラクタ内のコードが実行され、this を通じて新オブジェクトにプロパティやメソッドが追加される。
  4. この新オブジェクトが暗黙的に返される(コンストラクタが別のオブジェクトを明示的に返さない限り)。

これにより、同じ基本構造を持つ多くのオブジェクトを効率的に作成でき、リテラル構文を繰り返すよりもクリーンで管理しやすいコードになります。

2. プロトタイプ (Prototypes) の導入:効率的なメソッドの共有

コンストラクタの内部でメソッドを直接定義すること(例:this.greet = function() { ... };)は可能ですが、大量のオブジェクトを扱う場合には大きな欠点があります。

新しい Person オブジェクトを作成するたびに、全く新しい greet 関数のコピーが作成され、その特定のオブジェクトに保存されます。100個の Person オブジェクトがあれば、メモリ内には全く同じ内容の greet 関数が100個存在することになり、非常に非効率でメモリの無駄遣いです。

ここで登場するのが プロトタイプ (Prototypes) です。プロトタイプは、オブジェクト間でメソッドやプロパティを共有するための仕組みを提供し、メモリを節約してパフォーマンスを向上させます。

2.1 プロトタイプとは何か?

JavaScript のすべてのオブジェクトには、[[Prototype]] と呼ばれる特別な内部リンクがあり、別のオブジェクト(そのプロトタイプ)を指しています。

オブジェクトのプロパティやメソッドにアクセスしようとすると、JavaScript はまずそのオブジェクト自体を探します。見つからない場合、オブジェクトのプロトタイプを探しに行きます。それでも見つからなければ、さらにそのプロトタイプのプロトタイプへと、チェーンの終端(null)に達するまで探し続けます。このリンクの連なりを プロトタイプチェーン (Prototype Chain) と呼びます。

重要なのは、JavaScript のすべての関数(コンストラクタ含む)は、自動的に prototype プロパティ(内部の [[Prototype]] と区別するために小文字の 'p')を持っている点です。この prototype プロパティに何かを追加すると、そのコンストラクタで作成されたすべてのオブジェクトからアクセス可能になります。

2.2 プロトタイプにメソッドを定義する

すべてのインスタンス間でメソッドを共有するには、コンストラクタの prototype プロパティにメソッドを追加します。

Person コンストラクタを、プロトタイプを使ってリファクタリングしてみましょう:

// Person コンストラクタ
function Person(name, age) {
  this.name = name;
  this.age = age;
  // ここでは greet メソッドを定義しません!
}

// Person のプロトタイプに 'greet' メソッドを追加
// このメソッドはすべての Person オブジェクトで共有される
Person.prototype.greet = function() {
  console.log(`こんにちは、私の名前は ${this.name} です。今年で ${this.age} 歳になります。`);
};

// 2 つの新しい Person オブジェクトを作成
const person3 = new Person("チャーリー", 35);
const person4 = new Person("ダイアナ", 40);

person3.greet(); // 出力: こんにちは、私の名前は チャーリー です。今年で 35 歳になります。
person4.greet(); // 出力: こんにちは、私の名前は ダイアナ です。今年で 40 歳になります。

この改善版のポイント:

  • greet 関数は Person.prototype 上に一度だけ定義されます。Person オブジェクトがいくつあっても、メモリ内の greet 関数のコピーは一つだけです。
  • person3.greet() を呼び出す際、JavaScript はまず person3 自体に greet があるか探します。
  • 見つからないため、プロトタイプチェーンを辿り、Person.prototype を探しに行きます。
  • そこで greet を見つけて実行します。

重要なのは、共有されたメソッド内部でも this は呼び出し元の特定のオブジェクト(person3person4)を正しく指すという点です。これにより、共通のメソッドから各インスタンス固有のプロパティにアクセスできます。

3. 実戦的な例とデモンストレーション

3.1 示例 1:プロトタイプメソッドを持つ Book コンストラクタ

図書館管理システムを想定します。書籍にはタイトル、著者、ページ数が必要です。情報を表示するメソッドと、既読状態を切り替えるメソッドを共有させます。

// 1. Book コンストラクタの定義
function Book(title, author, pages) {
  this.title = title;
  this.author = author;
  this.pages = pages;
  this.isRead = false; // 初期状態は未読(各インスタンス固有)
}

// 2. 共有される振る舞いをプロトタイプに追加
Book.prototype.displayInfo = function() {
  console.log(`「${this.title}」著者:${this.author}, 全 ${this.pages} ページ。 ${this.isRead ? '既読' : '未読'}`);
};

Book.prototype.toggleReadStatus = function() {
  this.isRead = !this.isRead;
  console.log(`「${this.title}」の読書ステータスを更新しました:${this.isRead ? '既読' : '未読'}`);
};

// 3. インスタンスの作成
const book1 = new Book("ホビット", "J.R.R. トールキン", 310);
const book2 = new Book("1984", "ジョージ・オーウェル", 328);

console.log("--- 初期情報 ---");
book1.displayInfo(); // 出力: ... 未読

console.log("\n--- ステータス更新 ---");
book1.toggleReadStatus();
book1.displayInfo(); // 出力: ... 既読

3.2 示例 2:ディーラー在庫管理のための Car コンストラクタ

自動車ディーラーのアプリを想定します。ブランド、モデル、年式、価格を管理し、詳細取得や割引適用(その車のみに影響)のメソッドを定義します。

// Car コンストラクタ
function Car(make, model, year, price) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.price = price;
  this.isSold = false;
}

// 共有メソッド
Car.prototype.getDetails = function() {
  return `${this.year}年製 ${this.make} ${this.model} - $${this.price.toLocaleString()} ${this.isSold ? '(売約済)' : '(在庫あり)'}`;
};

Car.prototype.applyDiscount = function(percentage) {
  if (percentage > 0 && percentage < 100) {
    const discountAmount = this.price * (percentage / 100);
    this.price -= discountAmount; // 特定のインスタンスの price プロパティを更新
    console.log(`${percentage}% の割引を適用しました。新価格:$${this.price.toLocaleString()}`);
  } else {
    console.log("無効な割引率です。");
  }
};

const car1 = new Car("Toyota", "Camry", 2022, 28000);
const car2 = new Car("Ford", "F-150", 2021, 45000);

console.log("--- 在庫詳細 ---");
console.log(car1.getDetails());

console.log("\n--- 割引適用 ---");
car1.applyDiscount(5); 
console.log(car1.getDetails());

この例では、getDetailsapplyDiscount メソッドはメモリ上に一度だけ保存されますが、それぞれのオブジェクトはコンストラクタで定義された自身の状態(this.pricethis.isSold)を保持し、変更することができます。これが JavaScript における効率的なオブジェクト設計の基本です。