JavaScriptプロトタむプチェヌン完党ガむド

JavaScriptプロトタむプチェヌン完党ガむド

D
dongAuthor
6 min read

JavaScriptのプロトタむプチェヌンの栞心抂念から実践的な掻甚たでメモリ効率の良い継承構造ずprototype、protoの違いをコヌド䟋で分かりやすく解説したす。

JavaScriptを扱う開発者なら必ず理解すべき抂念の䞀぀が、たさにプロトタむプチェヌンです。このメカニズムはJavaScriptのオブゞェクト指向プログラミングの栞心であり、コヌドの再利甚性ずメモリ効率を倧幅に向䞊させるこずができたす。

倚くの開発者がES6のクラス構文に慣れ、プロトタむプの重芁性を芋過ごしがちです。しかし、クラス構文も内郚的にはプロトタむプを掻甚しおいるため、プロトタむプチェヌンを正しく理解すれば、JavaScriptの動䜜原理をより深く把握できたすよ。

この蚘事では、プロトタむプチェヌンの基本抂念から実際の掻甚方法たで、実務ですぐに応甚できる内容を扱っおいきたす。コヌド䟋ず共に段階的に説明したすので、最埌たで぀いおくれば、あなたもプロトタむプチェヌンマスタヌになれるはずです

JavaScriptずオブゞェクト指向プログラミングの出䌚い

JavaScriptはマルチパラダむム蚀語です。関数型プログラミングも可胜ですし、オブゞェクト指向プログラミングもサポヌトしおいたす。しかし、JavaやC++のような䌝統的なオブゞェクト指向蚀語ずは異なる方匏を採甚しおいたす。

䌝統的なオブゞェクト指向蚀語はクラスベヌスの継承を䜿甚したす。クラスずいう蚭蚈図を䜜成し、その蚭蚈図を基にオブゞェクトを生成する方匏ですね。䞀方、JavaScriptはプロトタむプベヌスの継承を䜿甚したす。

// 䌝統的なクラスベヌス蚀語の抂念実際のJavaScriptコヌドではありたせん
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}が鳎きたす。`);
  }
}

// JavaScriptのプロトタむプベヌスのアプロヌチ
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name}が鳎きたす。`);
};

JavaScriptでは、すべおのオブゞェクトが他のオブゞェクトから盎接継承できたす。これこそがプロトタむプベヌスプログラミングの栞心です。

プロトタむプベヌスプログラミングずは

プロトタむプベヌスプログラミングは、クラスなしでオブゞェクトを生成し、継承を実装できるプログラミングパラダむムです。JavaScriptでは、すべおのオブゞェクトが他のオブゞェクトを参照できる隠しリンクを持っおいたす。

プロトタむプベヌスプログラミングの特城

  1. 動的継承: 実行時にオブゞェクトのプロトタむプを倉曎できたす。
  2. 柔軟性: クラス宣蚀なしでオブゞェクト間の継承関係を構築できたす。
  3. メモリ効率: 共通のメ゜ッドをプロトタむプで共有し、メモリを節玄できたす。
// プロトタむプベヌス継承の䟋
const animal = {
  type: '動物',
  speak() {
    console.log(`${this.name}は${this.type}です。`);
  }
};

const dog = Object.create(animal);
dog.name = 'ワンちゃん';
dog.breed = '珍島犬';

dog.speak(); // "ワンちゃんは動物です。"

この方匏の利点は、非垞に柔軟であるこずです。必芁に応じおオブゞェクトの構造を動的に倉曎でき、耇雑なクラス階局なしで継承を実装できたす。

関数のprototypeプロパティを深く掘り䞋げる

JavaScriptにおいお、関数は特別なオブゞェクトです。すべおの関数はprototypeずいうプロパティを持っおいたす。このprototypeプロパティは、その関数がコンストラクタずしお䜿甚される際に䜜られるオブゞェクトが継承するプロトタむプオブゞェクトを指したす。

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

console.log(Person.prototype); // {constructor: ƒ}
console.log(typeof Person.prototype); // "object"

関数が生成されるず、JavaScript゚ンゞンは自動的にその関数のprototypeオブゞェクトを䜜成したす。このプロトタむプオブゞェクトは、基本的にはconstructorプロパティのみを持っおいたす。

prototypeプロパティの掻甚

prototypeプロパティを掻甚するず、コンストラクタ関数で䜜ったすべおのむンスタンスが共通しお䜿甚できるメ゜ッドやプロパティを定矩できたす。

function Car(brand, model) {
  this.brand = brand;
  this.model = model;
}

// プロトタむプにメ゜ッドを远加
Car.prototype.getInfo = function() {
  return `${this.brand} ${this.model}`;
};

Car.prototype.start = function() {
  console.log(`${this.getInfo()}の゚ンゞンをかけたす。`);
};

const myCar = new Car('ヒュンダむ', '゜ナタ');
const yourCar = new Car('キア', 'K5');

myCar.start(); // "ヒュンダむ ゜ナタの゚ンゞンをかけたす。"
yourCar.start(); // "キア K5の゚ンゞンをかけたす。"

// 2぀のオブゞェクトが同じメ゜ッドを共有しおいるか確認
console.log(myCar.start === yourCar.start); // true

この方匏の倧きな利点はメモリ効率です。もし各むンスタンスごずにメ゜ッドを個別に定矩するず、むンスタンスの数だけメ゜ッドがメモリ䞊に生成されおしたいたすよね。しかし、プロトタむプを䜿えば、䞀぀のメ゜ッドをすべおのむンスタンスで共有できたす。

__proto__ず[[Prototype]]内郚スロットの理解

JavaScriptのすべおのオブゞェクトは[[Prototype]]ずいう内郚スロットを持っおいたす。これは、そのオブゞェクトのプロトタむプを指す隠されたリンクです。倚くのブラりザでは、この内郚スロットに__proto__ずいうプロパティでアクセスできるようになっおいたす。

__proto__ずprototypeの違い

この二぀は混同されがちですが、明確に区別する必芁がありたすよ。

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

const dog = new Animal('ワンちゃん');

// prototype: 関数のプロパティコンストラクタ関数にのみ存圚
console.log(Animal.prototype); // {constructor: ƒ}

// __proto__: オブゞェクトのプロパティすべおのオブゞェクトに存圚
console.log(dog.__proto__); // {constructor: ƒ}

// この二぀は同じオブゞェクトを指したす
console.log(Animal.prototype === dog.__proto__); // true

実際のプロトタむプチェヌンの確認

function Shape(type) {
  this.type = type;
}

Shape.prototype.getType = function() {
  return this.type;
};

function Circle(radius) {
  Shape.call(this, '円');
  this.radius = radius;
}

// CircleがShapeを継承するように蚭定
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.getArea = function() {
  return Math.PI * this.radius * this.radius;
};

const myCircle = new Circle(5);

// プロトタむプチェヌンの確認
console.log(myCircle.__proto__ === Circle.prototype); // true
console.log(myCircle.__proto__.__proto__ === Shape.prototype); // true
console.log(myCircle.__proto__.__proto__.__proto__ === Object.prototype); // true

プロトタむプチェヌンの動䜜原理

プロトタむプチェヌンは、JavaScriptがオブゞェクトのプロパティやメ゜ッドを探すメカニズムです。察象のオブゞェクトに目的のプロパティがなければ、プロトタむプチェヌンをたどっお探しに行きたす。

プロパティ怜玢プロセス

  1. たずオブゞェクト自身のプロパティから怜玢
  2. なければ__proto__が指すプロトタむプオブゞェクトで怜玢
  3. それでもなければ、そのプロトタむプのプロトタむプで怜玢
  4. Object.prototypeたでたどっおもなければundefinedを返す
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name}が鳎きたす。`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log('ワンワン');
};

const myDog = new Dog('ポチ', '珍島犬');

// プロパティ怜玢プロセスのシミュレヌション
console.log(myDog.name); // 1段階: myDogオブゞェクトで発芋 - "ポチ"
console.log(myDog.breed); // 1段階: myDogオブゞェクトで発芋 - "珍島犬"
myDog.bark(); // 2段階: Dog.prototypeで発芋 - "ワンワン"
myDog.speak(); // 3段階: Animal.prototypeで発芋 - "ポチが鳎きたす。"
console.log(myDog.toString()); // 4段階: Object.prototypeで発芋

動的なプロトタむプ倉曎

プロトタむプは実行時に動的に倉曎できたす。これは非垞に匷力な機胜ですが、慎重に䜿う必芁がありたす。

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

const user1 = new User('キム・チョルス');
const user2 = new User('む・ペンヒ');

// 埌からプロトタむプにメ゜ッドを远加
User.prototype.greet = function() {
  console.log(`こんにちは、${this.name}です。`);
};

// すでに生成されたオブゞェクトも新しいメ゜ッドを䜿甚可胜
user1.greet(); // "こんにちは、キム・チョルスです。"
user2.greet(); // "こんにちは、む・ペンヒです。"

プロトタむプチェヌンの制玄ず泚意点

プロトタむプチェヌンは匷力ですが、いく぀かの制玄ず泚意点がありたす。

埪環参照の防止

JavaScriptはプロトタむプチェヌンでの埪環参照を蚱可したせん。

const obj1 = {};
const obj2 = {};

obj1.__proto__ = obj2;
// obj2.__proto__ = obj1; // TypeError: Cyclic __proto__ value

パフォヌマンスに関する考慮事項

プロトタむプチェヌンが長くなるほど、プロパティの怜玢時間が増加したす。特に頻繁にアクセスするプロパティの堎合、パフォヌマンスに圱響を䞎える可胜性がありたす。

// パフォヌマンスのためのキャッシング䟋
function ExpensiveOperation() {}

ExpensiveOperation.prototype.calculate = function() {
  if (this._cachedResult) {
    return this._cachedResult;
  }
  
  // 耇雑な蚈算を実行
  this._cachedResult = Math.random() * 1000;
  return this._cachedResult;
};

const operation = new ExpensiveOperation();
console.log(operation.calculate()); // 最初の呌び出し蚈算実行
console.log(operation.calculate()); // 2回目の呌び出しキャッシュされた結果を返す

プロパティの倉曎ず削陀の制限

プロトタむプチェヌンを通じおアクセスしたプロパティは読み取り専甚であり、倉曎や削陀は察象のオブゞェクトに盎接䜜甚したす。

const parent = { x: 1 };
const child = Object.create(parent);

console.log(child.x); // 1 (parentから継承)

child.x = 2; // childオブゞェクトに新しいxプロパティが生成される
console.log(child.x); // 2
console.log(parent.x); // 1 (倉曎されない)

delete child.x;
console.log(child.x); // 1 (再びparentから継承)

クラスベヌス vs プロトタむプベヌスの継承比范

ES6で導入されたクラス構文ず、䌝統的なプロトタむプベヌスの継承を比范しおみたしょう。

クラス構文 (ES6+)

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}が鳎きたす。`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log('ワンワン');
  }
}

const myDog = new Dog('ポチ', '珍島犬');

プロトタむプベヌスの実装

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

Animal.prototype.speak = function() {
  console.log(`${this.name}が鳎きたす。`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log('ワンワン');
};

const myDog = new Dog('ポチ', '珍島犬');

各方匏の長所ず短所

クラス構文の長所:

  • 構文が盎感的で読みやすい
  • 他のオブゞェクト指向蚀語ず䌌た構造
  • superキヌワヌドで芪クラスぞのアクセスが容易

プロトタむプベヌスの長所:

  • より柔軟な継承構造
  • ランタむムで動的に継承関係を倉曎可胜
  • メモリ効率をより现かく制埡可胜

プロトタむプチェヌンの実甚的な利点

メモリ節玄ずパフォヌマンス最適化

プロトタむプを掻甚するず、メ゜ッドをすべおのむンスタンスで共有するため、メモリを効率的に䜿甚できたす。

// 非効率な方法
function User(name) {
  this.name = name;
  this.greet = function() { // 各むンスタンスごずに関数を生成
    console.log(`こんにちは、${this.name}です。`);
  };
}

// 効率的な方法
function User(name) {
  this.name = name;
}

User.prototype.greet = function() { // すべおのむンスタンスが共有
  console.log(`こんにちは、${this.name}です。`);
};

// メモリ䜿甚量の比范
const users1 = Array(1000).fill().map((_, i) => new User(`ナヌザヌ${i}`));
// 最初の方法1000個のgreet関数がメモリに生成される
// 2番目の方法1個のgreet関数をすべおのむンスタンスが共有

コヌドの再利甚性向䞊

プロトタむプチェヌンを掻甚するず、共通の機胜を簡単に再利甚できたす。

// 共通機胜を持぀基本クラス
function BaseEntity() {}

BaseEntity.prototype.save = function() {
  console.log(`${this.constructor.name}を保存したす。`);
};

BaseEntity.prototype.delete = function() {
  console.log(`${this.constructor.name}を削陀したす。`);
};

// 特化したクラス矀
function User(name) {
  this.name = name;
}
User.prototype = Object.create(BaseEntity.prototype);
User.prototype.constructor = User;

function Product(name, price) {
  this.name = name;
  this.price = price;
}
Product.prototype = Object.create(BaseEntity.prototype);
Product.prototype.constructor = Product;

const user = new User('キム・チョルス');
const product = new Product('ノヌトPC', 1000000);

user.save(); // "Userを保存したす。"
product.delete(); // "Productを削陀したす。"

プロトタむプ管理メ゜ッド

Object.getPrototypeOfずObject.setPrototypeOf

これらのメ゜ッドを䜿甚しお、オブゞェクトのプロトタむプを安党に照䌚し、倉曎するこずができたす。

const animal = {
  type: '動物',
  speak() {
    console.log(`${this.name}は${this.type}です。`);
  }
};

const dog = { name: 'ワンちゃん' };

// プロトタむプを蚭定
Object.setPrototypeOf(dog, animal);

// プロトタむプを照䌚
console.log(Object.getPrototypeOf(dog) === animal); // true

dog.speak(); // "ワンちゃんは動物です。"

Object.createの掻甚

Object.createは、指定されたプロトタむプを持぀新しいオブゞェクトを生成する最もクリヌンな方法です。

const vehiclePrototype = {
  start() {
    console.log(`${this.brand} ${this.model}の゚ンゞンをかけたす。`);
  },
  
  stop() {
    console.log(`${this.brand} ${this.model}の゚ンゞンを切りたす。`);
  }
};

// プロトタむプを指定しおオブゞェクトを生成
const car = Object.create(vehiclePrototype, {
  brand: { value: 'ヒュンダむ', writable: true },
  model: { value: '゜ナタ', writable: true },
  year: { value: 2023, writable: true }
});

car.start(); // "ヒュンダむ ゜ナタの゚ンゞンをかけたす。"

プロトタむプチェヌンの安党な探玢

function walkPrototypeChain(obj) {
  const chain = [];
  let current = obj;
  
  while (current !== null) {
    chain.push(current.constructor?.name || 'Anonymous');
    current = Object.getPrototypeOf(current);
  }
  
  return chain;
}

function Animal(name) { this.name = name; }
function Dog(name, breed) { 
  Animal.call(this, name); 
  this.breed = breed; 
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const myDog = new Dog('ポチ', '珍島犬');
console.log(walkPrototypeChain(myDog)); 
// ['Dog', 'Animal', 'Object']

実践的なプロトタむプ継承の掻甚䟋

プラグむンシステムの実装

// 基本プラグむンクラス
function Plugin(name) {
  this.name = name;
  this.enabled = false;
}

Plugin.prototype.enable = function() {
  this.enabled = true;
  console.log(`${this.name}プラグむンが有効化されたした。`);
};

Plugin.prototype.disable = function() {
  this.enabled = false;
  console.log(`${this.name}プラグむンが無効化されたした。`);
};

// 特化したプラグむン
function AuthPlugin() {
  Plugin.call(this, 'Authentication');
}
AuthPlugin.prototype = Object.create(Plugin.prototype);
AuthPlugin.prototype.constructor = AuthPlugin;

AuthPlugin.prototype.authenticate = function(user) {
  if (!this.enabled) {
    throw new Error('認蚌プラグむンが無効化されおいたす。');
  }
  console.log(`${user}ナヌザヌを認蚌したす。`);
};

function LoggingPlugin() {
  Plugin.call(this, 'Logging');
}
LoggingPlugin.prototype = Object.create(Plugin.prototype);
LoggingPlugin.prototype.constructor = LoggingPlugin;

LoggingPlugin.prototype.log = function(message) {
  if (!this.enabled) return;
  console.log(`[LOG] ${message}`);
};

// 䜿甚䟋
const authPlugin = new AuthPlugin();
const loggingPlugin = new LoggingPlugin();

authPlugin.enable();
loggingPlugin.enable();

authPlugin.authenticate('キム・チョルス');
loggingPlugin.log('ナヌザヌがログむンしたした。');

ミックスむンパタヌンの実装

// ミックスむンオブゞェクト
const Flyable = {
  fly() {
    console.log(`${this.name}が飛びたす。`);
  }
};

const Swimmable = {
  swim() {
    console.log(`${this.name}が泳ぎたす。`);
  }
};

// ミックスむンを適甚するヘルパヌ関数
function mixin(target, ...sources) {
  sources.forEach(source => {
    Object.getOwnPropertyNames(source).forEach(name => {
      if (name !== 'constructor') {
        target.prototype[name] = source[name];
      }
    });
  });
  return target;
}

// 基本動物クラス
function Animal(name) {
  this.name = name;
}

// アヒルクラス飛ぶこずも泳ぐこずもできる
function Duck(name) {
  Animal.call(this, name);
}
Duck.prototype = Object.create(Animal.prototype);
Duck.prototype.constructor = Duck;

// ミックスむン適甚
mixin(Duck, Flyable, Swimmable);

const donald = new Duck('ドナルド');
donald.fly(); // "ドナルドが飛びたす。"
donald.swim(); // "ドナルドが泳ぎたす。"

デコレヌタヌパタヌンの実装

// 基本のコヌヒヌクラス
function Coffee() {
  this.cost = 1000;
  this.description = '基本のコヌヒヌ';
}

Coffee.prototype.getCost = function() {
  return this.cost;
};

Coffee.prototype.getDescription = function() {
  return this.description;
};

// デコレヌタヌの基本クラス
function CoffeeDecorator(coffee) {
  this.coffee = coffee;
}

CoffeeDecorator.prototype.getCost = function() {
  return this.coffee.getCost();
};

CoffeeDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription();
};

// 具䜓的なデコレヌタヌ
function MilkDecorator(coffee) {
  CoffeeDecorator.call(this, coffee);
}
MilkDecorator.prototype = Object.create(CoffeeDecorator.prototype);
MilkDecorator.prototype.constructor = MilkDecorator;

MilkDecorator.prototype.getCost = function() {
  return this.coffee.getCost() + 500;
};

MilkDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription() + '、ミルク';
};

function SugarDecorator(coffee) {
  CoffeeDecorator.call(this, coffee);
}
SugarDecorator.prototype = Object.create(CoffeeDecorator.prototype);
SugarDecorator.prototype.constructor = SugarDecorator;

SugarDecorator.prototype.getCost = function() {
  return this.coffee.getCost() + 200;
};

SugarDecorator.prototype.getDescription = function() {
  return this.coffee.getDescription() + '、砂糖';
};

// 䜿甚䟋
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new SugarDecorator(myCoffee);

console.log(myCoffee.getDescription()); // "基本のコヌヒヌ、ミルク、砂糖"
console.log(myCoffee.getCost()); // 1700

プロトタむプチェヌンをマスタヌする

プロトタむプチェヌンは、JavaScriptの栞心抂念の䞀぀です。これを正しく理解すれば、より効率的で柔軟なコヌドを曞くこずができたす。

重芁なポむントをたずめるず次のようになりたす

  1. プロトタむプベヌスの継承: JavaScriptはクラスベヌスではなく、プロトタむプベヌスの継承を䜿甚したす。
  2. メモリ効率: プロトタむプを掻甚するこずでメ゜ッドを共有し、メモリを節玄できたす。
  3. 動的な特性: 実行時にプロトタむプを倉曎でき、非垞に柔軟なプログラミングが可胜です。
  4. チェヌン探玢: プロパティを怜玢する際、プロトタむプチェヌンをたどっお怜玢するメカニズムを理解する必芁がありたす。
  5. 安党な䜿甚: Object.getPrototypeOf、Object.setPrototypeOf、Object.createなどのメ゜ッドを掻甚しお、安党にプロトタむプを管理できたす。

プロトタむプチェヌンをマスタヌすれば、ES6のクラス構文もより深く理解でき、JavaScriptラむブラリやフレヌムワヌクの内郚動䜜原理も把握しやすくなりたす。実務で遭遇する耇雑な継承構造やデザむンパタヌンも自信を持っお扱えるようになるでしょう。

今からあなたのプロゞェクトでプロトタむプチェヌンを積極的に掻甚しおみおください。よりクリヌンで効率的なコヌドが曞けるようになるはずです

JavaScriptプロトタむプチェヌン完党ガむド