【JavaScript 入門講座】JavaScriptはクラスじゃない!?プロトタイプ継承をやさしく解説

はじめに

JavaScriptによるゲーム開発は、継続していますが、P2Pの開発がメインになり、やや難航しています。

本日は、P2Pネットワークに接続されたユーザーリストを作成するまで出来ましたが、暑さと疲労にやられUI実装するまで気力が回りませんでした。

今回は、consoleログを何度か出力するとよく見ると思われる [[Prototype]] という謎のオブジェクトについて、AIに聞いたところ回答が面白かったのでそれを記事にしてみます。

この記事では、そんな [[Prototype]] を入り口に、JavaScriptの「プロトタイプ継承」 について、初心者にもわかりやすく解説していきます。

🧠 基礎:プロトタイプ継承ってなに?

JavaScriptは、JavaやC++のようなクラスベースの言語とは違い、プロトタイプベースの継承という仕組みを採用しています。

🔁 クラスベースとプロトタイプベースの違い

比較項目 クラスベース(Java/C++など) プロトタイプベース(JavaScript)
継承の単位 class(クラス) object(オブジェクト)
継承の方法 extends / サブクラス定義 Object.create() / __proto__
実体の生成 new クラス() Object.create(元オブジェクト)
継承関係の構築時期 コンパイル時(静的) 実行時(動的に継承関係を変更可能)

JavaScriptでも class 構文は存在しますが、内部的にはプロトタイプベースで動作しています。 つまり、オブジェクトがオブジェクトを継承するという仕組みは、JavaScriptに深く根付いた哲学なのです。


🧬 プロトタイプ継承の仕組み

JavaScriptでは、すべてのオブジェクトは「親」となる別のオブジェクトを参照できます。 この「親オブジェクト」が [[Prototype]] です。

const base = { a: 1 };

// baseを継承した新しいオブジェクトを作成
const derived = Object.create(base);

console.log(derived.a); // → 1(baseから継承)

このとき、derivedbase[[Prototype]] として参照しており、 自分に a が無ければ base.a を見るという動作になります。


🔗 プロトタイプチェーンの仕組み

const animal = {
  speak() {
    console.log("...");
  }
};

const dog = Object.create(animal);
dog.speak = function () {
  console.log("ワン!");
};

const shiba = Object.create(dog);

shiba.speak(); // → "ワン!"

このように shiba → dog → animal という「プロトタイプチェーン」が構築されており、 プロパティやメソッドが見つかるまで親をたどる仕組みになっています。


Object.create() の役割

const base = { hello: "こんにちは" };
const derived = Object.create(base);

console.log(derived.hello); // → "こんにちは"
  • Object.create(obj) は、obj[[Prototype]] とする新しいオブジェクトを作る関数
  • クラス不要で「元となるオブジェクト」を継承できる柔軟な設計
  • 実行時に自由に継承関係を作れるのが強み

class構文の正体:実はプロトタイプ

JavaScriptには class 構文があり、クラスベース言語のように見える書き方ができます:

class Dog {
  speak() {
    console.log("ワン!");
  }
}

const pochi = new Dog();
pochi.speak(); // → "ワン!"

一見すると、JavaやC++と同じような「クラス継承」っぽく見えますよね?

でも実はこの class 構文、見た目がそうなっているだけで内部では prototype を使ったプロトタイプ継承が行われています。


🔬 中身を確認してみよう

console.log(typeof Dog); // → "function"
console.log(Dog.prototype.speak); // → 関数が出る(Dogのインスタンスが継承するメソッド)
console.log(Object.getPrototypeOf(pochi) === Dog.prototype); // → true

つまり、class はただのシンタックスシュガー(見た目をキレイにするための構文)にすぎず、 結局は prototype を使った継承をしているのです。


prototype / proto / [[Prototype]] の違い

JavaScript初心者が必ず混乱するこの3つ。 それぞれの役割を明確にしておきましょう。

名前 正体 所有者 役割・用途
prototype 普通のオブジェクト 関数(特にコンストラクタ) インスタンスに継承させたいプロパティを定義するためのもの
__proto__ アクセス可能なプロパティ すべてのオブジェクト [[Prototype]]への参照(非推奨だが便利)
[[Prototype]] 内部スロット(非公開) すべてのオブジェクト 実際に継承をたどるリンク(プロトタイプチェーン)

実例で理解する

function Animal() {}
Animal.prototype.speak = function() {
  console.log("...");
};

const cat = new Animal();

console.log(cat.__proto__ === Animal.prototype); // true
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
  • Animal.prototype は、インスタンス(cat)の親として使われるオブジェクト
  • cat.__proto__ は cat が参照しているプロトタイプ([[Prototype]])への実質的な窓口

まとめ:それぞれの使いどころ

目的 使うもの
クラスの継承を定義する class(裏で prototype
関数にインスタンス用のメソッドを定義する function.prototype
オブジェクトの親を確認・取得する __proto__(非推奨) or Object.getPrototypeOf()
親オブジェクトに新たなプロパティを付ける Object.setPrototypeOf()(推奨)

補足:__proto__は使ってもいいの?

実行環境ではほぼ問題なく動作しますが、ECMAScriptの標準では非推奨とされています。

公式にはこちらを使うのが推奨です:

Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, proto);

プロトタイプチェーンの深堀

JavaScriptでは、プロパティやメソッドを探すときに「プロトタイプチェーン」をたどる という仕組みが使われています。

たとえば、次のコード:

const grandparent = { greet() { console.log("こんにちは(祖父)"); } };
const parent = Object.create(grandparent);
const child = Object.create(parent);

child.greet(); // → "こんにちは(祖父)"

この場合、child には greet() が存在しないため…

child → parent → grandparent → greet() を見つける!

という順でプロトタイプチェーンをたどって メソッドが呼ばれます。


チェーンを視覚的に見てみよう

console.log(Object.getPrototypeOf(child) === parent); // true
console.log(Object.getPrototypeOf(parent) === grandparent); // true

🔁 このように、オブジェクト同士が「親子関係」でつながっているのがプロトタイプ継承の基本です。


注意:見つからなければ undefined

チェーンをいくらたどっても該当プロパティが見つからなければ undefined が返されます。

console.log(child.sayHi); // → undefined(チェーン内のどこにもない)

Object.create(null) のような応用

Object.create() は元になるオブジェクトを明示的に指定できますが、 特殊なケースとして「プロトタイプを完全に持たないオブジェクト」を作ることもできます。

const pure = Object.create(null);

console.log(pure.toString); // → undefined(toString は Object.prototype にあるため)

これ、いつ使うの?

1. 安全なハッシュマップ(辞書)として使いたいとき

通常のオブジェクトだと、toString__proto__ などがプロトタイプから継承されてしまい、意図しない挙動を起こすことがあります。

const map = {};
map["toString"] = "うっかり上書き"; // 💣 プロトタイプ汚染の危険

const safeMap = Object.create(null);
safeMap["toString"] = "これは安全"; // OK

2. ライブラリ・フレームワークの内部実装で使われる

  • キーが非常に多いデータ構造
  • 安全性・速度重視のオブジェクト操作
  • 特に Map が導入される前(ES6以前)では定番だった

プロトタイプの“深み”を理解する

概念 説明
プロトタイプチェーン 親 → 祖父 → 曽祖父…とたどってプロパティを探す仕組み
Object.create(obj) obj を親とする新しいオブジェクトを作成
Object.create(null) **親なし(純粋な辞書)**を作れる特別な使い方
継承が見つからない場合 undefined になる

まとめ

今回の記事では、JavaScriptの謎多き存在 [[Prototype]] を入り口に、 プロトタイプ継承の仕組みと、それにまつわるキーワードたち を一気に整理しました。

押さえておきたいポイント

  • JavaScriptは プロトタイプベース の言語。class も実は表現だけ。
  • [[Prototype]]継承元を示す内部スロット
  • Object.create(obj) で、obj を親にするオブジェクトを動的に作れる
  • __proto__prototype[[Prototype]] にはそれぞれの役割がある
  • Object.create(null) はプロトタイプなしの“純粋な辞書”

JavaScriptの継承は、慣れると非常に柔軟でパワフル。 「プロトタイプチェーン」を意識することで、オブジェクトの挙動がぐっと読みやすくなりますよ。


付録:おすすめデバッグ方法([[Prototype]]を可視化する)

コンソールで出る [[Prototype]]: Object ── 実はただの飾りじゃありません。中身を見て、仕組みを掘るチャンスです。

Chrome DevTools の使い方

const base = { a: 1 };
const derived = Object.create(base);
console.log(derived);

👆 この derived を DevTools の コンソールで展開すると…

  • a は自分に無い → [[Prototype]] の先をたどって base.a にアクセス
  • まさに、継承のリアルタイム確認ができます!

チェーンを確認したいときは:

Object.getPrototypeOf(obj)
console.dir(obj)

なども使えば、親オブジェクトの構造も確認できます。


不要な継承を防ぎたいときは:

const dict = Object.create(null); // 純粋なマップオブジェクト

ハッシュテーブル用途では Map より高速な場合も。


補足:P2P通信中でも役立つ?

たとえば、受信データのオブジェクトに __proto__ が残ってるかどうかをチェックすれば、 プロトタイプ汚染(Prototype Pollution) 対策にもなります。

if (Object.getPrototypeOf(data) === null) {
  // 安全な形式
}