iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

プロトタイプとクラスの関係

■ はじめに

はじめまして!

株式会社iimonでフロントエンドエンジニアをしている白水です。

この記事では、JavaScriptの「プロトタイプ」と「クラス」の関係性について書いていきます。

■ オブジェクトを生成する方法

# Pythonでオブジェクトを作る
class Person:
  def __init__(self, name):
      self.name = name

  def greeting(self):
      return 'こんにちは、' + self.name

taro = Person('太郎')
print(taro.greeting()) # > こんにちは、太郎

オブジェクトを生成する方法には「クラスベース言語」と「プロトタイプベース言語」があります。

PythonRubyなどが「クラスベース」に分類され「クラス」という設計図をもとにnew 演算子などを用いてオブジェクトが作られます。

一方で、JavaScriptは「プロトタイプベース」に分類されます。

プロトタイプベースでは「プロトタイプ」という仕組みがあり、その仕組みを使ってオブジェクトが作られています。

ES6(ES2015)からJSにもクラスが追加されましたが、その裏側では「プロトタイプ」という仕組みが動いています。

■ コンストラクタ関数って何?

// コンストラクタ関数
function Person(arg) {
  this.name = arg;
}

const taro = new Person("太郎");
taro.name; // > "太郎"

JavaScriptのES5バージョン(2009年に出たバージョン)までは、コンストラクタ関数とnew演算子を用いて、オブジェクトを作っていました。

class Person {
  constructor(arg) {
    this.name = arg;
  }
}

const taro = new Person("太郎");
console.log(taro.name); // > "太郎"

コンストラクタ関数は、クラスの中で定義する「constoructor」と同じ動きをする関数のことを指しています。

そのため「コンストラクタ関数」でのthisと、クラスのコンストラクタ内でのthisは共にnewしたオブジェクト自身を指しています。

このようにコンストラクタ関数と、classの中の「constoructor」は同じようなものです。

ちなみに、constoructorとはクラスをnew演算子を使ってインスタンス化した際に、1番最初に自動的に呼び出される特別なメソッドのことです。

コンストラクター - JavaScript | MDN

■ プロトタイプの特徴

◆ コンストラクタ関数を定義すると勝手にprototypeプロパティが定義される

// コンストラクタ関数
function Person() {}

 // 関数もオブジェクトの一種なので、プロパティに値を設定できる
Person.userName = '太郎';
Person.userName; // > 太郎

JavaScriptでは「関数」もオブジェクトの一種です。(非プリミティブ型に分類されるため)

そのため、関数をオブジェクトのように扱うことができます。

「オブジェクトのように扱うことができる」というのは、関数にプロパティやメソッドを持たせることができるということです。

// コンストラクタ関数
function Person() {
}

// Personコンストラクタ関数にprototypeプロパティがあるか?
console.log('prototype' in Person); // > true

// prototypeプロパティがある
Person.prototype; // > {constructor: ƒ}

// prototypeプロパティに設定されている値はオブジェクト
typeof Person.prototype; // > object

その中で、プロトタイプというのは、関数を定義すると勝手にprototypeプロパティが定義されます。

また、prototypeプロパティに設定されている値はobjectであることがわかります。

// コンストラクタ関数
function Person(arg) {
  this.name = arg;
}

Person.prototype.sayName = function() {
  console.log(`名前は、${this.name}です。`);
}

const jiro = new Person('次郎');
jiro.sayName(); // > 名前は、次郎です。
// クラス
class Person {
  constructor(arg) {
    this.name = arg;
  }

  sayName() {
    console.log(`名前は、${this.name}です。`);
  }
}

const jiro = new Person('次郎');
jiro.sayName(); // > おはよう、次郎

prototypeに登録したsayNameという無名関数内で使われるthisは、呼び出し元のオブジェクトを参照しているため、prototypeに登録された関数はPersonクラスのsayNameメソッドと同じ役割を担っています。

String.prototype.at() - JavaScript | MDN

インスタンス化するとprotptypeプロパティへの参照がprotoにコピーされる

function Person() {
}

Person.prototype.greet = function() {
  console.log('こんにちは');
}

const human = new Person();

// 同じオブジェクトを参照している
Person.prototype === human.__proto__; // > true

// __proto__を通してメソッドを呼び出している
human.greet(); // > こんにちは

human.__proto__.greet(); // > こんにちは

// 省略してもしなくても、同じメソッドを参照している
human.greet === human.__proto__.greet; // > true

newでコンストラクタ関数からインスタンスを作ると、コンストラクタ関数のprototypeに設定されているオブジェクトへの参照が、インスタンス__proto__という

特別なプロパティにコピーされます。(参照のコピー)

そのため、Person.prototypehuman.__proto__のオブジェクトは同じであり、メソッドを実行するときは、human.__proto__.greet();のように__proto__を通して実行できます。

■ クラスでもprototypeという仕組みが裏側で動いている

class Person {
    greet() {
        console.log('こんにちは');
  }
}

// クラスにもprototypeが勝手にできる
Person.prototype // > {constructor: ƒ, greet: ƒ}

const instance = new Person();

// 同じオブジェクトを参照している
Person.prototype === instance.__proto__; // > true

// クラスでも__proto__を通してメソッドを実行している
instance.greet(); // > こんにちは
instance.__proto__.greet(); // > こんにちは

クラスでも裏側ではprototypeという仕組みが動作しています。

そのため、クラスを定義してもprototypeというプロパティが存在し、newすることで、__proto__という特別なプロパティが定義されて、prototypeへの参照が保持されます。

■ MDNでStringコンストラクタ関数のメソッドを確認してみる

String.prototype; // > String {'', constructor: ƒ, anchor: ƒ, at: ƒ, big: ƒ, …}

const str1 = new String(' Hello ');
str1.__proto__; // > String {'', constructor: ƒ, anchor: ƒ, at: ƒ, big: ƒ, …}
str1.__proto__ === String.prototype; // > true
str1.trim(); // > 'Hello'

// 文字列リテラルの場合は、裏側でStringコンストラクタのオブジェクトに変換されている。
const str2 = ' Hello ';
str2; // > ' Hello ' 
str2.trim();

MDNを見てみると、prototypeという文字をよく見ると思います。

trimというメソッドは、Stringコンストラクタのprototypeプロパティの値としてtrimというメソッドが設定されていることを表しています。

なので、Stringコンストラクタをnewすると、インスタンス__proto__というプロパティの値に、Stringコンストラクタのprototypeプロパティの値への参照がコピーされるので、文字列オブジェクトからtrimメソッドが呼び出せます。

String.prototype.trim() - JavaScript | MDN

■まとめ

  • コンストラクタ関数を定義すると、prototypeというプロパティが勝手に作られる。
  • newすると、コンストラクタ関数のprototypeというプロパティへの参照がインスタンス__proto__という特別なプロパティに保持される。
  • クラスで定義しても、プロトタイプという仕組みが裏側で動作している。
  • prototypeという仕組みを通して、クラスの継承やメソッドのオーバーライドなども実現されている。

オブジェクトのプロトタイプ - ウェブ開発を学ぶ | MDN