
はじめに
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から継承)
このとき、derived
は base
を [[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) {
// 安全な形式
}
💬 コメント