カエデ@30代無職&未経験からフロントエンドエンジニア

30代無職&未経験からフロントエンドエンジニアを目指すブログ

クラス [JavaScript]

もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。

今回は、クラスについて調べていきたいと思います。

クラスを学ぶきっかけ

前回までのJavaScript学習では、コンストラクタ関数やファクトリ関数について調べて分かってきたことをまとめてきました。prototypeチェーンの仕組みなど、基本的な部分については大分理解が進んできたように感じています。

クラスについては、ReactやTypeScriptを学んだ際に使う機会があったのですが、何となく写経する程度でちゃんとした仕組みは分かっていませんでした。

このまま分からないままだと他人に説明できないコーディングになってしまうので、一度調べてみる必要があると思い、クラスについて掘り下げてみることにしました。

クラスの学び方

私は今回、クラスについて説明しているページをひたすら読み漁ることで理解を深めていきました。何となく把握出来てきたら、実際にコーディングをしていくとさらに学習が進むと思います。

クラスを使った書き方は「JavaScript class use case」や「JavaScript MVC 」などと検索すると、色々出てきます。To Do リストや、簡単なゲームを作りながら学んでいけるチュートリアルなども見つかるのではないかと思います。

クラスとは

クラスは、コンストラクタ関数などと同じく、インスタンスを作る際のひな型となる関数です。ES6以降に導入された、新しいコンストラクタ関数の書き方です。

基本的な部分は従来のコンストラクタ関数と変わりませんが、prototypeメソッドがより分かりやすく書けるようになったり、classキーワードを使って関数を設定することで、より読みやすいコードが書けるようになりました。

クラス関数の設定方法

従来のコンストラクタ関数と違う点がいくつかあります。

その違いを見てみるために、同じ役割をするひな型を、クラスとコンストラクタ関数の両方で書いてみたいと思います。

ここでは例として、新しいbookオブジェクトを作り出す関数を作成します。

コンストラクタ関数の書き方

インスタンスに持たせたいプロパティを、thisキーワードを使って設定します。

function BookConstructor(title, author, year) {
      this.title = title;
      this.author = author;
      this.year = year;
}

クラスの書き方

クラスの場合も同じく、thisキーワードを使用してプロパティを設定します。その際、それらのプロパティはconstructorの中に設定します。

class BookClass {
   constructor(title, author, year) {
      this.title = title
      this.author = author
      this.year = year
   }
}

インスタンスを作る際は、どちらも同じくnewキーワードを付けて関数を呼び出します。

const book1 = new BookConstructor("人間失格", "太宰治", 1948);
f:id:kaedefrontend:20200815161125j:plain
const book2 = new BookClass("人間失格", "太宰治", 1948);
f:id:kaedefrontend:20200815161140j:plain

クラスの中に書いてある「constructor」とは

コンストラクタ(constructor)は、newキーワードを使用してインスタンスを作成する際に自動的に呼び出されるメソッドです。
呼び出された際、インスタンスにプロパティを割り当てるなどの初期設定を行います。
コンストラクタはクラスの中に1つだけ設定することが出来ます。

prototypeメソッドの定義の仕方

こちらも、従来のコンストラクタ関数でのやり方とクラスでのやり方を比較しながら見ていきます。

コンストラクタ関数の場合

まずコンストラクタ関数の場合、メソッドを関数内に定義すると、作られたインスタンスの中にもメソッドが付与されます。

function BookConstructor(title, author, year) {
      this.title = title;
      this.author = author;
      this.year = year;
      this.calculateYear = function() {
          return `この本は${2020 - this.year}年前に発行されました`;
      }
}

const book = new BookConstructor("羅生門", "芥川龍之介", 1915);
f:id:kaedefrontend:20200819160850j:plain

インスタンスを作成するたびにメソッドが付与されるとメモリを圧迫してしまうので、メソッドを定義する場合は、prototypeとして設定するのが望ましい方法です。

メソッドをprototypeとして設定するように書き直します。

function BookConstructor(title, author, year) {
      this.title = title;
      this.author = author;
      this.year = year;
}
BookConstructor.prototype.calculateYear = function() {
      this.calculateYear = function() {
          return `この本は${2020 - this.year}年前に発行されました`;
      }
}

上記のコンストラクタ関数を使用し、新たにインスタンスを作成します。

const book = new BookConstructor("人間失格", "太宰治", 1948);

作成されたインスタンスを見てみると、上記で設定したメソッドcalculateYearは、prototypeの中に入っていることが分かります。

f:id:kaedefrontend:20200819161951j:plain

クラスの場合

クラスの場合、クラス関数内に設定したメソッドは、自動的にprototypeとして設定されます。

なのでコンストラクタ関数の時のようにBookConstructor.prototype.calculateYear()と、prototypeとして分けて設定する必要がありません。

下記の例では、BookClassクラスの中にメソッドcalculateYear()が設定してあります。

class BookClass {
      constructor(title, author, year) {
            this.title = title;
            this.author = author;
            this.year = year;
      }

      // クラス関数内でメソッドを設定する      
      calculateYear() {
          return `この本は${2020 - this.year}年前に発行されました`;
      }
}

このクラスを使用し、インスタンスを作成します。

const book = new BookClass("人間失格", "太宰治", 1948);

インスタンスの詳細を見てみると、メソッドcalculateYear()は、prototypeとして設定されていることが分かります。

f:id:kaedefrontend:20200819162306j:plain

extends と super

クラスを作った後に、サブクラス(子クラス)を設定したい場合は、extendsキーワードを使って新たにクラスを設定します。

サブクラスは、親クラスのプロパティやメソッドを継承することが出来ます。

例えば、親クラス Book の下に、サブクラス Comic を持たせる場合、下記のように設定します。

class Book {
      constructor(title, author, year) {
            this.title = title;
            this.author = author;
            this.year = year;
      }
}
class Comic extends Book {
    constructor(title, author, year, magazine) {
        super(title, author, year)
        this.magazine = magazine;
    }
}

サブクラスの constructor の中では super というキーワードを使うことで、親クラスのプロパティを呼び出すことが出来ます。

この際、super は constructor の一番上に書く必要があります。一番上に書かない場合はエラーになってしまいます。

上記のサブクラスを元にインスタンスを作成し、詳細を確認してみます。

const comic = new Comic("美少女戦士セーラームーン","武内直子", 1992, "なかよし");
f:id:kaedefrontend:20200819162447j:plain

サブクラスのComicが、親クラスBookのプロパティtitleauthoryearを継承していることが確認できました。

getter と setter

getter と setterとは

getter と setterは、プロパティのように扱うことが出来るメソッドです。メソッドでありながらプロパティのような振舞いをするため、アクセッサプロパティと呼ばれています。

オブジェクトを扱う際、プロパティの値を直接変更することは一般的に望ましくないとされています。

そこで getter や setter を使うことで、実際にプロパティの値には手を加えないまま、あたかも変更されたかのように見せることが出来るのです。

例えるならば、Photoshopで言うところの「レイヤーマスク」をかけるイメージです。 Photoshopではレイヤーマスクを使用することにより、元の画像を切り取っていなくても、マスクした部分だけ 画像が切り取られたように見せることが出来ます。

getter や setter も同様に、値を返す前にメソッドのロジックを適用させ、そのカスタマイズされた値を、プロパティの値として扱います。

getterの使い方

getter を設定する際は、メソッド名の前にget と付けます。

getterは、何かしらの値を返すように設定します。

getter について調べている時、このようなサンプルを何度も目にしました。

class Book {
    constructor(title, author, year) {
        this._title = title;
        this._author = author;
        this._year = year;
    }

    get title() {
        return this._title;
    }
    get author() {
        return this._author;
    }
    get year() {
        return this._year;
    }
}

const book = new Book("人間失格", "太宰治", 1948);
f:id:kaedefrontend:20200819162639j:plain

初めは、いったいなぜわざわざgetterを設定するのか分かりませんでした。

上記の書き方であれば、コンストラクタの中の this._title = title と書いてあるところを this.title = title に変更してしまえば、 わざわざgetterを設定しなくても book.title で本のタイトルにアクセス出来ると思っていました。

JavaScriptでは、変数名の先頭に_を付けると、「変更してはいけない変数」であると明示することが出来るそうです。
なので上のコードの場合だと、実際のプロパティ名とは違った getter を設定し、その getter を使ってプロパティにアクセスすることで、元のプロパティが直接アクセスされることを防いでいます。

book.titleというのは、titleプロパティにアクセスしているのではなく、titleという名前の getter にアクセスしている状態です。

f:id:kaedefrontend:20200819162757j:plain

setter の使い方

setterを設定する際は、メソッド名の前に setと付けます。

setter は、何らかの値を引数に取る必要があります。

インスタンスを作る場合は、最初は引数を設定せずに空のインスタンスを作成し、setter を介してプロパティの値を設定します。

setter を作ったら、getter も作ります。getter がないと、setter で登録したプロパティにアクセスすることが出来ません。

また setter と getter の名前は、プロパティの名前と違うものにしなければなりません。 もし同じにしてしまうと、Maximum Call stack size exceeded というエラーが出てきてしまいます。

下記の例の場合だと、setter と getter は title、プロパティ名は _title と区別しています。

class Book {
    constructor(title, author, year) {
        this._title = title; // title は getter の名前
        this._author = author; // author は getter の名前
        this._year = year; // year は getter の名前
    }
    get title() {
        return this._title;
    }
    get author() {
        return this._author;
    }
    get year() {
        return this._year;
    }
    set title(newTitle) {
        this._title = newTitle;
    }
    set author(newAuthor) {
        this._author = newAuthor;
    }
    set year(newYear) {
        this._year = newYear;
    }
}

インスタンス名.setter名 で setter メソッドを実行し、各プロパティを設定します。

const book = new Book();

book.title = "人間失格";
book.author = "太宰治";
book.year = 1948;
f:id:kaedefrontend:20200819162955j:plain

getter や setter に機能を持たせる

getter や setter には、条件分岐やバリデーション機能を持たせることも出来ます。

例えば

1.setter を使い、本のタイトルが1文字以下の場合は『短すぎるタイトル』というタイトルに設定する
2.getter を使い、作者が登録されていなかった場合は、作者名は「作者不明」と表示する

というような動作をさせたい場合、下記のように設定します。

class Book {
    constructor(title, author) {
        this._title = title;
        this._author = author;
    }
    get title() {
        return this._title;
    }
    get author() {
        if (this._author === undefined) {
            return '作者不明';
        } else {
            return this._author;
        }
    }
    set title(newTitle) {
        if (newTitle.length < 3) {
            this._title = '短すぎるタイトル';
        } else {
            this._title = newTitle;
        }
    }
    set author(newAuthor) {
        this._author = newAuthor;
    }
}

インスタンスを作り、setter で下記のようにプロパティを登録します。

const book = new Book();

book.title = "本";

getter を使ってアクセスすると、条件分岐によって設定された値が取得されます。

f:id:kaedefrontend:20200819170057j:plain

メソッド vs getter

getter について調べていた時にさらに気になったのが、どういう時にgetterを使用し、どういう時にメソッドを使用するのかという点です。

2つの大まかな違いは、getter から返された値は「オブジェクトのデータ」として扱われ、通常のメソッドを実行した場合は「関数から返された値」として扱われるとのことです。

調べた印象では、2つの使い分けに厳密な決まりはなく、メソッドとして設定した方が分かりやすいという人や、getter でプロパティとして扱った方が無駄な括弧を書かなくて済むから楽だという人など、様々いるようでした。

2つの違いを見るために、本が発行されてからの経過年数を、メソッドを使った方法と getter を使った方法の両方で取得してみたいと思います。

メソッドの場合

メソッドを使う場合は、インスタンス名.メソッド名()と書きます。

下記の場合だと、book.yearsFromPublication()のように実行します。

class Book {
    constructor(title, year) {
        this._title = title;
        this._year = year;
    }
    get title() {
        return this._title;
    }
    get year() {
        return this._year;
    }
    set title(newTitle) {
        this._title = newTitle;
    }
    set year(newYear) {
        this._year = newYear;
    }
    // メソッド
    yearsFromPublication() {
        return `${2020 - this.year}`;
    }
}

const book = new Book("人間失格", 1948);
f:id:kaedefrontend:20200819163341j:plain

getter の場合

メソッドの先頭に getと付けると、メソッドは getter に変わります。

getter の場合は、インスタンス名.getter名でアクセスすることが出来ます。

下記の場合だと、book.yearsFromPublicationのようにアクセスします。

class Book {
    constructor(title, year) {
        this._title = title;
        this._year = year;
    }
    get title() {
        return this._title;
    }
    get year() {
        return this._year;
    }
    set title(newTitle) {
        this._title = newTitle;
    }
    set year(newYear) {
        this._year = newYear;
    }
    // getter
    get yearsFromPublication() {
        return `${2020 - this.year}`;
    }
}

const book = new Book("人間失格", 1948);
f:id:kaedefrontend:20200819163613j:plain
f:id:kaedefrontend:20200819163625j:plain

静的メソッド

静的メソッドは、オブジェクトを扱う際のユーティリティ関数(効用関数)として使用することができます。

静的メソッドは、メソッド名の前に static と付けて設定します。

下の例では、静的メソッドmyFavを設定し、その後呼び出しています。

class Book {
    constructor() { }

    static myFav(genre) {
        return `私は${genre}が好きです。`;
    }
}
f:id:kaedefrontend:20200819163734j:plain

静的メソッドはクラスのプロパティです。
getter や setter がインスタンスに属しているのに対し、静的メソッドはインスタンスには属していません。
なので、newキーワードを使用してインスタンスを作らなくても、クラス名.静的メソッド名()と書くだけで実行することが出来ます。

先ほど作成したクラスからインスタンスを作成し、詳細を確認してみます。

class Book {
    constructor() { }

    static myFav(genre) {
        return `私は${genre}が好きです。`;
    }
}

const book = new Book();

インスタンスの中身を見てみても、静的メソッド myFavは見当たりません。

f:id:kaedefrontend:20200819165121j:plain

Bookクラスの中身を確認してみると、こちらにmyFavがありました。

f:id:kaedefrontend:20200819165341j:plain

静的メソッドの指すthis

静的メソッド内でthisキーワードを使った場合、thisはクラス自身を指します。静的メソッドのthisは、インスタンスのプロパティとは紐づいていません。

例として、静的メソッド内でthisを使い、 year プロパティの値を使おうとしてみます。

class Book {
    constructor(year) {
        this.year = year;  
    }

    static calculateYearStatic() {
        return 2020 - this.year;
    }
}

const book = new Book("人間失格", 1948);
f:id:kaedefrontend:20200819164840j:plain

calculateYearを実行した場合、NaNになってしまいます。これは、calculateYear内のthisが、インスタンスを向いておらず、yearプロパティの値にアクセス出来ないからです。

今度は、thisを表示させる静的メソッド printThis を作って実行し、thisの正体を確認してみます。

class Book {
    constructor(year) {
        this.year = year;  
    }

    static printThis() {
        console.log(this);
    }
}
f:id:kaedefrontend:20200819164953j:plain

thisは、Bookクラスを指していることが確認できました。

クラス内で、thisを使って静的メソッドから別の静的メソッドにアクセスする

クラス内である静的メソッドから別の静的メソッドを呼び出したい場合は、thisを使って参照することが出来ます。

class StaticMethodDemo {
    static staticMethodOne() {
        return 'staticメソッド1 + ';
    }

    static staticMethodTwo() {
        return this.staticMethodOne() + 'staticメソッド2';
    }
}
f:id:kaedefrontend:20200819164704j:plain

静的メソッドを使う理由

静的メソッドはユーティリティ関数として使用できる、というような説明を何度も見かけたのですが、それだけでは静的メソッドを使う理由が分かりませんでした。

クラスの中にわざわざ静的メソッドを設定しなくても、普通にクラスの外に関数を作れば同じ処理が出来てしまうからです。

下記のコードでは、全く同じ処理を静的メソッドと通常の関数で実行しています。

class Book {
    constructor() { }

    static myFav(genre) {
        return `私は${genre}が好きです。`;
    }
}

    function myFavFunc(genre) {
        return `私は${genre}が好きです。`;
}
f:id:kaedefrontend:20200819164541j:plain

ではなぜ静的メソッドを使う理由があるかというと、関数をクラス内のスコープに入れておくことによって、クラスの外にある変数名との不要な衝突を防ぐことが出来るからということのようです。

クラス内に関数が入っていれば、例え同じ名前の関数をクラスの外に作ってしまってもエラーにならずに済みます。

下記の例では myFav という変数名を2回使っていますが、1つはBookクラスの中に入っていて実行する際は Book.myFav() と呼び出すため、エラーにはなりません。

class Book {
    constructor() { }

    static myFav(genre) {
        return `私は${genre}が好きです。`;
    }
}

function myFav(genre) {
        return `私は${genre}が好きです。`;
}

まとめ

実は今回ちゃんと調べるまで、クラスのことをほぼ理解していませんでした。constructorって何だろう、superとは何だろう、そういうレベルでした。

クラスの書き方を学んだ後に、以前少しだけ勉強したTypeScriptのチュートリアルのメモを見返してみたのですが、自分がいかに理解もせずただ写経していたかを思い知らされました。今考えるとかなりの時間を無駄にしたと思います。ただクラスを学んだ今、もう一度やり直せば以前よりも理解出来るだろうという期待感は持てるようになりました。

今回は触りを少し勉強しただけですが、実際にコーディングをしながら色々調べ、クラスの書き方に慣れていきたいと思います。

参考にしたサイト

JavaScript Classes: An In-Depth look|Medium
非常に分かりやすい解説です。Part1からPart4までの解説に加え、応用編としてクラスを使用した非常にシンプルなゲームのコーディング方法を紹介しています。

クラス |JavaScript Primer
クラスについての詳しい解説が載っています。

Class basic syntax |The Modern JavaScript Tutorial
こちらもクラスについての解説が載っています。

How To Use Class in Javascript|AppDividend
コンストラクタ関数とクラスでの書き方を比較して説明しているページです。



JavaScriptを教えていただいている、もりた先生の詳細はこちらです。

[もりけん塾]
ブログ:http://kenjimorita.jp
Twitterhttps://twitter.com/terrace_tech