JavaScript | プロトタイプと継承の「中の仕組み」や「落とし穴」

javascrpit JavaScript
スポンサーリンク

ここまで理解しているなら、プロトタイプと継承の「中の仕組み」や「落とし穴」を丁寧に掘り下げていける段階。

初心者でも「仕組みを感覚でつかめる」ように、図解イメージ+コード実験付きで説明していく。

ステップ1:プロトタイプチェーンの「中身」を見てみよう

まずは図でイメージ。

alice → Person.prototype → Object.prototype → null

これが「プロトタイプチェーン」。

  • alice には sayName がない
  • alice.__proto__(つまり Person.prototype)を探す
  • → なければさらに Object.prototype
  • → 最後は null で終了

実際に確認できる。

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const alice = new Person("Alice");

console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
JavaScript

つまり、JavaScript の「継承」はオブジェクトをチェーン状につなぐ探索ルールなんだ。

ステップ2:プロパティ探索のしくみ

オブジェクトにプロパティをアクセスすると、エンジンは次の順で探す:

  1. そのオブジェクト自身(自身のプロパティ)
  2. プロトタイプ(__proto__
  3. さらにそのプロトタイプのプロトタイプ…
  4. null に行き着いたら探索終了(undefined を返す)

例:

const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;

console.log(dog.barks); // 1. 自身にある → true
console.log(dog.eats);  // 2. 自身に無い → animal から借りる → true
console.log(dog.toString); // 3. Object.prototype から借りる
JavaScript

ステップ3:プロパティの「設定(書き込み)」はどう動く?

読み取り(obj.prop)はチェーンを辿るけど、
書き込み(obj.prop = ...)は常に自分自身に新しいプロパティを作る

const parent = { value: 10 };
const child = Object.create(parent);

child.value = 20;
console.log(child.value);  // 20(自分のプロパティが新しくできた)
console.log(parent.value); // 10(親は変わらない)
JavaScript

これが「シャドウイング(shadowing)」と呼ばれる動作。
つまり「見かけ上、親を上書きしたように見えて実は別物」。

ステップ4:プロパティの「性質」(ディスクリプタ)

プロパティには値だけでなく「メタ情報」がある。
それが プロパティディスクリプタ (Property Descriptor)

主な属性:

属性意味デフォルト値
value実際の値undefined
writable値を書き換え可能かfalse(define時はfalse、リテラル定義ではtrue)
enumerablefor…in や Object.keys に出てくるかtrue
configurabledelete や再定義が可能かtrue

例で確認:

const obj = { a: 1 };
console.log(Object.getOwnPropertyDescriptor(obj, "a"));
JavaScript

出力(例):

{
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
}
JavaScript

これを使うと、プロパティの動作を細かく制御できる。

ステップ5:defineProperty で特殊なプロパティを作る

const obj = {};
Object.defineProperty(obj, "secret", {
  value: "hidden",
  writable: false,      // 書き換え禁止
  enumerable: false,    // for...in に出ない
  configurable: false   // 削除不可
});

console.log(obj.secret); // "hidden"
obj.secret = "changed";
console.log(obj.secret); // 書き換えられないので "hidden" のまま
JavaScript

ステップ6:プロトタイプの変更

✅ 確認する

Object.getPrototypeOf(obj);
JavaScript

✅ 変更する

Object.setPrototypeOf(obj, newProto);
JavaScript

ただし ⚠️注意:

  • 実行時にプロトタイプチェーンを動的に変えると パフォーマンスが落ちる(エンジンの最適化が効かなくなる)。
  • 普通は最初から Object.create(proto) で設計時に決めておく方が安全。

ステップ7:クラス構文の裏側で起きていること

class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}

const alice = new Person("Alice");
JavaScript

これは実際には次のように展開される。

function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function() {
  console.log(this.name);
};
JavaScript

だから Person.prototype にメソッドを追加すれば、クラスでも同じように全インスタンスが共有する。

ステップ8:バグにつながりやすいケース(実例)

ケース1:共有オブジェクトを prototype に置く

function User(name) {
  this.name = name;
}
User.prototype.data = [];

const u1 = new User("A");
const u2 = new User("B");

u1.data.push("X");
console.log(u2.data); // ["X"] 😱 全員で同じ配列を共有!
JavaScript

👉 対策:
コンストラクタで新しい配列を作るようにする:

function User(name) {
  this.name = name;
  this.data = []; // 個別に持つ
}
JavaScript

ケース2:this が変わる

👉 対策:

const greetFn = user.greet.bind(user);
greetFn(); // "Alice"
JavaScript

テップ9:安全に継承するパターン(中級向け)

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

function Dog(name) {
  Animal.call(this, name); // 親のコンストラクタ呼び出し
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  console.log(`${this.name} barks`);
};

const dog = new Dog("Max");
dog.speak(); // Max barks
JavaScript

ES6 クラスでは👇と同じ意味になる:

class Animal {
  constructor(name) { this.name = name; }
  speak() { console.log(`${this.name} makes a sound`); }
}
class Dog extends Animal {
  speak() { console.log(`${this.name} barks`); }
}
JavaScript

まとめ

概念役割
prototypeコンストラクタ関数が持つ設計図(インスタンスが参照する)
[[Prototype]]__proto__実際の「親オブジェクト」へのリンク
Object.create(proto)特定の親を持つオブジェクトを作る
Object.getPrototypeOf() / setPrototypeOf()プロトタイプの確認・変更
defineProperty()プロパティの動作を細かく設定
継承 (extends)クラスでプロトタイプチェーンを簡単に構築する方法
タイトルとURLをコピーしました