【JS】クラスの解説

18 min read 2

はじめに

「インストールしたライブラリがクラス表記になっていて分からない」
「Qiita や Zenn の記事を参考にしたいけど、クラスのコードが読めない」

と、私自身アプリケーションを作る際に困ったので、クラスとはなんなのかをまとめます。
初学者の参考になれば幸いです。

※コメントしてご指摘頂きましたが、現在はクラスを用いた書き方は主流ではないようです。
※この記事は「積極的にクラスを使って書こう!」と推奨するものではありません。

目次

  • クラスとは
  • クラスの書き方
  • クラスからオブジェクトを生成する
  • クラス継承
  • getter と setter と static
  • プライベートプロパティ
  • クラスを用いたチェーンメソッド
  • クラスにおける this の参照先
  • クラスの内部で何が行われているのか
  • まとめ:クラスのメリット
  • 参考文献

クラスとは

クラスとはオブジェクトを作るための雛形です。

え?オブジェクトなんて、const obj = {...}という形で作ってきたじゃないかって?

その通りです。
でも、「同じプロパティを持つけれども、違う値を持つオブジェクトをたくさん作る」 なんて時には、全て先程の書き方で定義するのは大変ですよね。

というわけで、
「○○ と △△ って要素(プロパティ)を持つお部屋(オブジェクトの雛形)を作ったよ!ここに家具(値)を置いていってそれぞれのマイルームを完成させてね!」
と、出来るのがクラスです。

そして、このお部屋には
配置する家具をそれぞれ決められる上に、
それぞれ お部屋のこだわりやアピールなどをメソッドとして定義できる
と言えばイメージが付きやすいでしょうか?

それでは、実際にどのようにして使っていくのか見てみましょう。

クラスの書き方

クラスの書き方には2種類あります。

  • クラス宣言
  • クラス式

という方法です。

クラス宣言

class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
        (this.プロパティ名 = 仮引数)
    }
    メソッド名(){
        メソッドの内容を記述する
    }
}

このような書き方になります。
実際に書いていくとこんな感じ。

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
}

クラス式

const 変数名 = class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
        (this.プロパティ名 = 仮引数)
    }
    メソッド名(){
        メソッドの内容を記述する
    }
}

クラス名の省略もできます。

const 変数名 = class {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
        (this.プロパティ名 = 仮引数)
    }
    メソッド名(){
        メソッドの内容を記述する
    }
}

実際に書くとこんな感じ

const MyRoom = class {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
};

注意点

ほとんど書き方は変わりませんが、
クラス式ではクラスを再宣言することができる
という特徴があることを覚えておきましょう。

また、クラスでのメソッドは、関数式では記述できないということに注意してください。
オブジェクトのメソッドでは、:で区切ることができましたが、クラスのメソッドでは区切りが無いため、()=>{}function(){}という書き方はシンタックスエラー(構文エラー)となります。

尚、どちらの場合でも
クラス名は先頭を大文字(キャメルケース)にする
という慣習があります。

「クラス名をキャメルケースにしておくと、インスタンス(クラスから生成したオブジェクト)は小文字の名前にしておけば被らないから、分かりやすいよね。」という理由からです。

クラスからオブジェクトを生成する

では、先程作ったクラスからオブジェクトを生成してみます。

この時、
クラスからオブジェクトを作ることをインスタンス化
生成されたオブジェクトをインスタンス
と呼ぶことを覚えておきましょう。

インスタンス化を行うには、new 演算子を使います。

インスタンス化

const インスタンス名 = new クラス名(値1, 値2, 値3);

実際にコードを書いてみます。

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
}

const myRoom = new MyRoom("だんぼ", "ガラス机", "テンピュール");
console.log(myRoom); //MyRoom {name: "だんぼ", desk: "ガラス机", bed: "テンピュール"}
myRoom.roomName(); //この部屋はだんぼの部屋です
myRoom.style(); //ベッドは絶対にテンピュールというこだわりがあります!

const hiyokoRoom = new MyRoom("ひよこ王子", "黄色い机", "ぴよぴよベッド");
console.log(hiyokoRoom); //MyRoom {name: "ひよこ王子", desk: "黄色い机", bed: "ぴよぴよベッド"}
hiyokoRoom.roomName(); //この部屋はひよこ王子の部屋です
hiyokoRoom.style(); //ベッドは絶対にぴよぴよベッドというこだわりがあります!

このように1つのクラスから、同じプロパティとメソッドを持った、だんぼさん と ひよこ王子 のオブジェクトを作ることができました。
オブジェクトの持つメソッドも問題なく使用出来ています。

クラス継承

さて、では MyRoom クラスから派生したクラス
GuestRoom(プロパティに date を追加し、「○ 日に御宿泊の予定です」とコンソールに表示するメソッドを追加)
を定義したい時はどうしたらいいでしょう?

MyRoom クラスをもう一度書いて、
クラス名を GuestRoom に変えて、
プロパティを追加して、
メソッドも追加して…

とするのは、ちょっと面倒ですし、何度も同じコードが出てくるのはコードが読みにくいですよね。

こんな時はクラス継承を使うと解決できます。

クラス継承とは、
他のクラスのプロパティやメソッドを継承すること
です。

クラス継承の書き方

クラス継承はextendsを使って行い、superで継承元のクラスを呼び出します。

class 継承先のクラス名 extends 元のクラス名 {
    constructor(継承先のクラスに取る仮引数) {
        super(元のクラスの仮引数);

        追加したいプロパティを this.プロパティ名 = 仮引数; で追記する

    }

    追加したい、又は上書きしたいメソッドを書いていく

}

では、実際に GuestRoom クラスを追加してみましょう。

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
}

//extendsでMyRoomを継承する
class GuestRoom extends MyRoom {
  //MyRoomのプロパティ+追加しだdateプロパティを引数に取る
  constructor(name, desk, bed, date) {
    //superでMyRoomを実行する
    super(name, desk, bed);
    //追加したプロパティ
    this.date = date;
    //!!constructor内では必ずsuperを先頭に持ってこないとエラーが出ます!!
  }
  //roomNameメソッドの内容を書き換える
  roomName() {
    console.log(`この部屋は${this.name}様がご予約されました`);
  }
  //予約日時を表示するstayメソッドを追記する
  stay() {
    console.log(`${this.date}に御宿泊の予定です`);
  }
}

const danbo = new GuestRoom(
  "だんぼ",
  "ガラス机",
  "テンピュール",
  "2021年5月31日"
);

console.log(danbo); //GuestRoom {name: "だんぼ", desk: "ガラス机", bed: "テンピュール", date: "2021年5月31日"}
danbo.roomName(); //この部屋はだんぼ様がご予約されました
danbo.stay(); //2021年5月31日に御宿泊の予定です
//extendsによって元のクラスのメソッドも継承されているため、style()も使用できる
danbo.style(); //ベッドは絶対にテンピュールというこだわりがあります!

extends はプロパティだけでなく、メソッドも継承するため、元のクラスにのみ書かれている style メソッドも問題なく使えるようになっています。
また、constructor 内ではsuperによる元のクラスの呼び出しを先頭に持ってこなければエラーになってしまうので、ご注意ください。

クラスを丸ごとコピーして追記していくと恐らく GuestRoom は 20 行程になってしまうかと思うんですが、クラス継承を使うことで、12 行で納めることができました。
今回は roomName メソッドの中身の上書きも行ったので 12 行でしたが、上書きも必要なければ 9 行で書けますね。
約半分のコード量になりますし、元は何のクラスだったのかが明示されているので、何のクラスを派生させたのかが一眼で分かるようになり、非常に見やすいコードになります。

getter と setter と static

通常、メソッドは、メソッド名()というコードでメソッドの呼び出し・実行を行わなければなりません。
ですが、クラスでは「メソッドを呼び出さずに使用できる特殊なメソッド」が存在します。
それが、getter, setterです。

…ちょっとイメージが湧きにくいですよね。
一つずつ説明していきましょう。

getter

getterは、「オブジェクトのプロパティを取得してきた時に、自動で行われるメソッド」と覚えるのが分かりやすいかな、と思います。

オブジェクトのプロパティを取得してくる時はオブジェクト名.プロパティ名で取得しますよね。
先程の MyRoom でインスタンス化されたオブジェクト、myRoomnameプロパティを取得した時に、自動で「おかえりなさい」と表示させることができる、という特殊なメソッドがgetterメソッドです。

getter の書き方

class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
    }
    メソッド名(){
        メソッドの内容を記述する
    }
    get プロパティ名となる仮引数() {
        仮引数のプロパティが取得された時の動作を記述する

        return プロパティ;
    }
}

const インスタンス = new クラス名();
インスタンス.プロパティ名となる仮引数;

では、実際に書いていきましょう。

class MyRoom {
  constructor(name, desk, bed) {
    this._name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
  get name() {
    //nameプロパティが取得された時の動作
    console.log("おかえりなさいませ");
    //プロパティを返す
    return this._name;
  }
}

const myRoom = new MyRoom("だんぼ", "ガラス机", "テンピュール");
console.log(myRoom); //MyRoom {_name: "だんぼ", desk: "ガラス机", bed: "テンピュール"}
//nameプロパティを呼び出す
console.log(myRoom.name);
//おかえりなさい
//だんぼ

さて、お気づきでしょうか、プロパティがthis._nameとなり、_がついていることを。
これは後ほど詳しく説明しますが、プライベートプロパティというものです。
ここでは、_namenameは全く別物の変数ということを意識して下さい。

myRoom オブジェクトを console.log した時にも分かるように、オブジェクトの中身は{_name: "だんぼ"}となっており、プロパティ名は_nameとなっています。
ではなぜ、myRoom.nameで値が取得できるのか。

実は getter を使うと、指定されたプロパティ取得時は getter を経由して値を取得します
ですので、myRoom.nameとすることで、「get name()を経由してくださいね」という指示が行われています。
getter を経由すると、return this._nameと書かれているため、「_nameプロパティを返しますよ。」となるわけです。
今回は値を返す前に「おかえりなさい」とコンソールに出力する指示が入っているため、

おかえりなさい
だんぼ

とコンソールに出力されることになります。

setter

先程のgetterは「オブジェクトのプロパティを取得してきた時に、自動で行われるメソッド」と説明しました。

逆に、setterは「オブジェクトのプロパティを代入した時に、自動で行われるメソッド」となります。

setter の書き方

class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
    }
    メソッド名(){
        メソッドの内容を記述する
    }
    set プロパティ名となる仮引数(setの仮引数) {
        仮引数のプロパティが取得された時の動作を記述する

        this.プロパティ = setの仮引数;
    }
}

const インスタンス = new クラス名();
インスタンス.プロパティ名となる仮引数 = 値;

それでは、ベッドを変更した時に「ベッドを ○○ に変更しました」と表示されるようにしてみましょう。
bedプロパティに値を代入した時に、console.log(...)が行われるsetterを作ります。

class MyRoom {
  constructor(name, desk, bed) {
    this._name = name;
    this.desk = desk;
    this._bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
  set bed(newBed) {
    //bedプロパティに値が代入された時の動作
    console.log(`ベッドが${newBed}に変更されました`);
    //新しい値を代入する
    this._bed = newBed;
  }
}

const myRoom = new MyRoom("だんぼ", "ガラス机", "テンピュール");
console.log(myRoom); //MyRoom {_name: "だんぼ", desk: "ガラス机", _bed: "テンピュール"}
//bedプロパティに値を代入する
myRoom.bed = "フランスベッド"; //ベッドがフランスベッドに変更されました
//値が代入されているかどうか確認する
console.log(myRoom); //MyRoom {_name: "だんぼ", desk: "ガラス机", _bed: "フランスベッド"}

ここでも、_bedbedが存在していることに注意しましょう。

プロパティ名は_bedなのにエラーが出ずに値が代入されているのは、myRoom.bedで「set bed()を経由してくださいね」という指示を行っているからです。
setter では「_bedプロパティにnewBedつまり、新しい値を代入してくださいね。」と指示されています。

その前にコンソールに「ベッドが変更されましたよ」と出力するように指示が出ているので、

ベッドがフランスベッドに変更されました

とコンソールに出力されています。

つまり、setter は指定されたプロパティ代入時には、setter を経由するメソッドとなります。

static

さて、staticは他の二種と少し毛色が異なります。
staticクラス内でのみ使用することができるキーワードであり、
new 演算子を用いてインスタンス化を行っていなくても、使用が可能な静的メソッドです。
スタティックメソッドとも言います。

静的メソッドですので、動的であるthisを使用することは出来ません。
(もしthisを使った場合、thisは「クラスの表記そのもの」を参照することになります)

static の書き方

class クラス名 {
    constructor(仮引数1, 仮引数2) {
        プロパティを記述する
    }
    メソッド名(){
        メソッドの内容を記述する
    }
    static 静的メソッド名() {
        静的メソッドの内容を記述する
    }
}

クラス名.静的メソッド名();
(インスタンス化は必要ない)

このように、メソッドの前にstaticをつけるだけで静的メソッドとなります。

それでは、MyRoom クラスに、「これは部屋に関する紹介です」とコンソールに出力するメソッドを作ってみましょう。

class MyRoom {
  constructor(name, desk, bed) {
    this._name = name;
    this.desk = desk;
    this._bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
  //静的メソッドを追加する
  static preface() {
    console.log("これは部屋に関する紹介です");
  }
}

//インスタンス化は行わない
MyRoom.preface(); //これは部屋に関する紹介です

MyRoom.style(); //(エラー)

このように、インスタンス化を行っていなくても、メソッドを呼び出すことができました。
一方、普通に定義した style メソッドは、呼び出してもエラーとなります。

プライベートプロパティ

ここで、先程少しだけ出てきたプライベートプロパティに関して少し説明しましょう。

プライベートプロパティとは構文として定義されているものではありません。

_から始まるプロパティは、クラスの外からは読み書きしない物として扱いましょう」
という暗黙のルールです。

ですので、実際にはconsole.log(myRoom._name)とした場合、プロパティに直接アクセスは可能です。

_から始まるプロパティは「プライベートプロパティ」。
クラスの内側でしかアクセスしないプロパティとして扱う。
と覚えておいてください。

クラスを用いたチェーンメソッド

例えば、
「これは〇〇の部屋です。」
「この部屋の机は〜〜〜です。」
「この部屋のベッドは〜〜〜です。」
「よかったら遊びに来てください」
と出力するメソッドをそれぞれ定義します。

この場合、全て出力するためにはどうしたらいいでしょうか?

インスタンス.メソッド1();
インスタンス.メソッド2();
インスタンス.メソッド3();
インスタンス.メソッド4();

と書くのは少し面倒ですよね。

できれば

インスタンス.メソッド1().メソッド2().メソッド3().メソッド4();

と出来ればコードがスッキリしますよね。

このように1つのインスタンスに対して連続してメソッドを呼び出すことを「チェーンメソッド」と言います。

ですが、普通に書いても、インスタンス.メソッド1()までは可能ですが、「メソッドはインスタンスでは無い」ので、1以降のメソッドを見つけ出すことはできません。
とすると、「メソッドの返り値がインスタンス」なら1以降のメソッドを発見することが可能になるのでは無いでしょうか。

というわけで、返り値を設定してみましょう。

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`これは${this.name}の部屋です`);
    return this;
  }
  deskDes() {
    console.log(`この部屋の机は${this.desk}です。`);
    return this;
  }
  bedDes() {
    console.log(`この部屋のベッドは${this.bed}です。`);
    return this;
  }
  message() {
    console.log("よかったら遊びに来てください");
  }
}

const myRoom = new MyRoom("だんぼ", "ガラス机", "テンピュール");
myRoom.roomName().deskDes().bedDes().message();
// これはだんぼの部屋です
// この部屋の机はガラス机です。
// この部屋のベッドはテンピュールです。
// よかったら遊びに来てください

メソッド内でのthisの参照先は、インスタンス化されたオブジェクトとなります。
今回は myRoom というインスタンスを指定してメソッドを呼び出しているので、thisは myRoom オブジェクトを参照します。
そのため、return thisとすることで、メソッドの終了時に myRoom オブジェクト自体は値として返ってきています。

メソッドの最後にreturn this;thisを返すと、チェーンメソッドが可能になる
と覚えておきましょう。

クラスにおける this の参照先

先程、

  • 静的メソッドではthisはクラスそのものを参照する
  • チェーンメソッドではreturn thisthisはオブジェクト自体を参照する
    と、言いました。
    これは一見矛盾しているように見えるかもしれません。

実は JavaScript では
thisはクラス専用のキーワードではなく、文脈によって参照するものが全く異なる
のです。

thisの参照先に関しては、こちらも併せて読むと良いかと思います。

https://zenn.dev/danbo/articles/6bc3153d3459d49f237d#参考文献

さて、先程述べた 2 点での違いは「インスタンス化されているかどうか」です。

「this の参照先まとめ」で説明しているように、
オブジェクトのメソッドとして実行された場合、this は "." の直前で指定しているオブジェクトを参照します

ですので、チェーンメソッドでは、
myRoom.roomName()で this は myRoom オブジェクトを参照します。
そして roomName()の返り値は this(= myRoom オブジェクト)なので、
実質、myRoom.deskDes()と続くようになり、
全てのチェーンメソッドで this は myRoom オブジェクトを参照します。

対して、静的メソッドではインスタンス化が行われていないので、
呼び出し方がクラス名.静的メソッド()となります。
クラスは飽くまでもオブジェクトでは無いため、レキシカルスコープであるクラスそのものを参照してしまうのです。

コールバック関数とクラスと this

では、問題として、コールバック関数でクラスメソッドの this が参照された場合を考えてみましょう。

setTimeout()を用いて、1 秒後にMyRoom.roomName()を実行する関数を作ります。

setTimeout()は第一引数に関数、第二引数にミリ秒を設定し、「第二引数ミリ秒後に、第一引数の関数を実行する」関数です。

window.name = "グローバルネーム";

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
}

const danbo = new MyRoom("だんぼ", "ガラス机", "テンピュール");

setTimeout(danbo.roomName, 1000); //この部屋はグローバルネームの部屋です

この時、roomName内のthis.nameは danbo オブジェクトの name プロパティである「だんぼ」を参照しません。
これは setTimeout()の内部では別の関数、仮にfunction()として関数が実行されているからです。

単なる関数として実行する場合、this はグローバルオブジェクトである window オブジェクトを参照する
ので、window オブジェクトが参照され、「グローバルネーム」がコンソールに出力されています。

このように、クラスメソッド内の this だからと言って、「必ずインスタンスを参照する」ということはなく、通常の this と同じ挙動を取ることに注意して下さい。

コールバック関数における this の扱いについては、前述した「this の参照先まとめ」で詳しくまとめてあります。

クラスの内部で何が行われているのか

実は、このクラスというものは ES6 から追加された機能です。
ES5 までは「コンストラクタ関数」と「プロトタイプ」を用いてオブジェクトの雛形を作っていたのですが、それを簡略化させて見やすくしたのが「クラス」です。
ですので、JS では
「クラス」は飽くまでもコンストラクタ関数をクラス表記で書けるようにしたもの
となり、内部ではコンストラクタ関数とプロトタイプを使った処理が行われています。

尚、
「とりあえずクラスの使い方が知りたいだけだ!」
という方は、この節は飛ばしてしまって構いません。

コンストラクタ関数

クラスと同様、コンストラクタ関数も
新しくオブジェクトを作成するための雛形となる関数
です。

今までの MyRoom クラスと同様のものをコンストラクタ関数に書き換えると以下のようになります。

function MyRoom(name, desk, bed) {
  this.name = name;
  this.desk = desk;
  this.bed = bed;
}

const danbo = new MyRoom("だんぼ", "ガラス机", "テンピュール");
const hiyoko = new MyRoom("ひよこ王子", "黄色い机", "ぴよぴよベッド");

console.log(danbo); //MyRoom {name: "だんぼ", desk: "ガラス机", bed: "テンピュール"}
console.log(hiyoko); //MyRoom {name: "ひよこ王子", desk: "黄色い机", bed: "ぴよぴよベッド"}

プロトタイプ

プロトタイプとは、オブジェクトに存在する特別なプロパティです。
クラスとは異なり、コンストラクタ関数ではプロトタイプの中にメソッドを追加します。

function MyRoom(name, desk, bed) {
  this.name = name;
  this.desk = desk;
  this.bed = bed;
}

//プロトタイプにroomNameというメソッドを追加する
MyRoom.prototype.roomName = function () {
  console.log(`この部屋は${this.name}の部屋です`);
};

const danbo = new MyRoom("だんぼ", "ガラス机", "テンピュール");
const hiyoko = new MyRoom("ひよこ王子", "黄色い机", "ぴよぴよベッド");

danbo.roomName(); //この部屋はだんぼの部屋です
hiyoko.roomName(); //この部屋はひよこ王子の部屋です

メソッドはどこに格納されているのか?

それでは、インスタンス化した時に、メソッドがどこに格納されているのか確認しましょう。
コンソールに danbo オブジェクトを表示してみます。

danboオブジェクトの内部

オブジェクトの内部には__proto__が格納されており、
さらにその内部に、roomName: f()メソッドが格納されています。

この時、インスタンスの内部に存在する__proto__の中身は、コンストラクタで定義された「prototype の参照」が入っています。

MyRoom.prototypedanbo.__proto__が同一のものか確認してみると、

このように true が返って来ます。

さて、では、この参照の仕方にどんなメリットがあるのでしょうか?
答えは、メモリの効率化です。

prototype を使わない時、インスタンスのメソッドは prototype の参照を行わないため、インスタンス毎にメソッドが追加されていきます。
インスタンス化される度に、メモリの内部にroomName: f()メソッドが増えていってしまうんですね。
その結果、メモリの中に複数個のroomName: f()メソッドが存在することになり、余計なメモリを消費してしまいます。

ですが、prototype を使うと、__proto__の内部にroomName: f()メソッドへの参照がコピーされていくだけになります。
roomName: f()メソッドを「家」と考えると、メソッドへの参照は「住所を書いたメモ」です。
家を何度も建てるより、どうせ同じものを使うなら、住所のメモを渡して行く方がコストが低いですよね。
こういう理由でメモリの効率化に繋がり、より効率的なプログラムになる、というわけです。

それでは、クラスの時はどういう構造になっているのか確認してみましょう。

class MyRoom {
  constructor(name, desk, bed) {
    this.name = name;
    this.desk = desk;
    this.bed = bed;
  }
  roomName() {
    console.log(`この部屋は${this.name}の部屋です`);
  }
  style() {
    console.log(`ベッドは絶対に${this.bed}というこだわりがあります!`);
  }
}

const danbo = new MyRoom("だんぼ", "ガラス机", "テンピュール");
const hiyoko = new MyRoom("ひよこ王子", "黄色い机", "ぴよぴよベッド");
console.log(danbo);

こちらのコードをコンソールに出力してみます。

このように、コンストラクタ関数と同様、
オブジェクトの内部には__proto__が格納されており、
さらにその内部に、roomName: f()メソッドが格納されています。

MyRoom.prototypedanbo.__proto__が同一のものという確認も取れました。

JS におけるクラスの動き

この節の冒頭で、
「クラス」は飽くまでもコンストラクタ関数をクラス表記で書けるようにしたもの
という説明をしました。

「コンストラクタ関数とプロトタイプ」と「クラス」を比較して分かる通り、
クラス表記を行っても、内部での処理は「コンストラクタ関数とプロトタイプ」と同様の処理が行われているのです。

ですので、コンストラクタ関数とプロトタイプを使用した時と同様、
メモリを効率的に使用できるということがクラスを使うメリットの1つとなります。

まとめ:クラスのメリット

  • 同じプロパティやメソッドを持つオブジェクトを複数使用する時に便利
  • クラス継承によるクラス間での機能の受け渡しが可能
  • 何度も同じコードを書く必要がなく、効率的にプログラムが組める
  • クラス内で処理を細かく分けることができるため、処理の流れが追いやすく、変更も容易
  • 処理を分け、シンプルなコードを書くことで可読性が上がる
  • プロトタイプによるメモリの効率化

参考文献

MDN
【JS】ガチで学びたい人のための JavaScript メカニズム
【JavaScript 入門】class 構文の使い方・書き方が分かるようになる方法!
JavaScript Primer