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

javascrpit JavaScript
スポンサーリンク

ここでは、初心者がハマりやすい典型的なバグを「再現→原因→対策」の流れで紹介します。

ケース1:プロトタイプでオブジェクトを共有してしまう

🧩 状況

全インスタンスが同じ配列・オブジェクトを参照してしまうパターン。

function Person(name) {
  this.name = name;
}

// ❌ NG: prototype に配列を直接置く
Person.prototype.hobbies = [];

const alice = new Person("Alice");
const bob = new Person("Bob");

alice.hobbies.push("reading");
console.log(bob.hobbies); // ["reading"] ← えっ!?
JavaScript

🔍 原因

hobbies は全インスタンスで同じ配列を 共有参照 している。
プロトタイプにあるオブジェクト型(配列・オブジェクト)は「1個しかない」から、誰かが変更すると全員に影響。

✅ 対策

配列やオブジェクトなど可変のデータは、prototype ではなく「各インスタンス内」に作る。

function Person(name) {
  this.name = name;
  this.hobbies = []; // インスタンスごとに新しく作る
}
JavaScript

ケース2:this の参照が意図せず変わる

🧩 状況

メソッドを変数に取り出して呼び出すと this が失われる。

const user = {
  name: "Alice",
  greet() {
    console.log("Hello, " + this.name);
  }
};

const greetFn = user.greet;
greetFn(); // "Hello, undefined"(またはエラー)
JavaScript

🔍 原因

this は「呼び出し方」で決まる。
greetFn() はどのオブジェクトからも呼ばれていないため、thisundefined になる(strict mode)か、window になる。

✅ 対策

  • bindthis を固定する
const greetFn = user.greet.bind(user);
greetFn(); // Hello, Alice
JavaScript
  • またはアロー関数で this を外側に固定する
const user = {
  name: "Alice",
  greet: () => console.log("Hello, " + this.name) // ただし、class 内では非推奨
};
JavaScript

ケース3:for...in がプロトタイプのプロパティまで列挙する

🧩 状況

意図しないプロパティが出てくる。

const parent = { shared: true };
const child = Object.create(parent);
child.own = true;

for (const key in child) {
  console.log(key);
}
// 出力:
// own
// shared ← prototype 上のプロパティまで!
JavaScript

✅ 対策

自分のプロパティだけを扱うようにする:

for (const key in child) {
  if (child.hasOwnProperty(key)) {
    console.log(key);
  }
}
JavaScript

あるいは最近は Object.keys()Object.entries() を使う方が安全。

ケース4:delete しても prototype 側の値が見えてしまう

🧩 状況

「削除したのに、まだ残ってる?」という現象。

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

delete child.value;
console.log(child.value); // 10 ← prototype 側の value が見える
JavaScript

🔍 原因

delete は「自分のプロパティを消す」だけ。
その後、プロトタイプチェーンを辿って parent.value が見つかるから表示される。

✅ 対策

hasOwnProperty() を確認して「自分の値かどうか」を判別する:

console.log(child.hasOwnProperty("value")); // false → 親の値を参照中
JavaScript

ケース5:継承時に constructor を失う

🧩 状況

Object.create で継承したときに、constructor が上書きされる。

function Animal(name) {
  this.name = name;
}
Animal.prototype.sayHi = function() {
  console.log(this.name);
};

function Dog(name) {
  Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype); // ← ここで constructor 消失
Dog.prototype.bark = function() {
  console.log("Woof!");
};

const dog = new Dog("Pochi");
console.log(dog.constructor === Dog); // false(Animalになってしまう)
JavaScript

✅ 対策

constructor を手動で復元する。

Dog.prototype.constructor = Dog;
JavaScript

ケース6:クラス継承でも同じような落とし穴(共有データ)

ES6 class でも、プロトタイプの仕組みを使っているため似た罠がある。

class Person {
  static shared = [];
  constructor(name) {
    this.name = name;
  }
}

const a = new Person("Alice");
const b = new Person("Bob");

Person.shared.push("data");
console.log(a.shared); // undefined
console.log(Person.shared); // ["data"] ← 静的プロパティだから共有!
JavaScript

もしインスタンスごとに別のデータを持ちたいなら、this.shared = []constructor の中で定義する。

ケース7:プロトタイプチェーンが長すぎてパフォーマンス低下

あまりに深いチェーンを作ると、プロパティ探索コストが増える。
これは「バグ」ではないが、思い通りに高速に動かない原因になりうる。

const base = { baseProp: 1 };
const level1 = Object.create(base);
const level2 = Object.create(level1);
const level3 = Object.create(level2);
const level4 = Object.create(level3);

console.log(level4.baseProp); // base まで4段も探索
JavaScript

現代のエンジンでは最適化されることも多いが、あまり深い継承構造は避けよう。

総まとめ:バグ防止の黄金ルール

ルール意味
可変データは prototype に置かない全インスタンスで共有されてしまう
this に依存する関数は bind or arrow呼び出し方で壊れる
列挙時は hasOwnProperty チェックprototype の値が混ざる
constructor を手動で復元Object.create 継承で失われる
プロトタイプチェーンは浅く深すぎる継承構造は遅くて複雑
タイトルとURLをコピーしました