memolu

いろいろメモってます

ES6 - Class を解説してみる その1

本連載について

本連載では少しづつES6について記載していこうと思います。 第五回はClassを学びます。 発表されてから久しいですが、まだ習得されていない方は今からでも勉強してみてはいかがでしょうか。

Class

Classとは「コンストラクタの定義」と「prototypeへのメソッド定義」をより簡単に行う構文です。
今までのjavascriptでは他のプログラミング言語に存在するようなclassという概念が存在しませんでした。これを担うために様々なライブラリなどが開発されてきましたが、今回ネイティブの機能として実装される運びになりました。
Class構文が追加されたお陰で、コンストラクタの継承やprototype定義などが初心者でも使いやすくなり、より人間が扱いやすい形になっています。 本機能は当初からES6の目玉としても注目されていて、最近ではFacebookが開発したReactでも非常に有効に活用されています。

ブラウザサポート

ブラウザサポートについては以下をご確認ください。

http://kangax.github.io/compat-table/es6/

今までの継承

Classの使い方を見る前に、今までのクラス生成パターンを見てみましょう。
これから紹介するコードは理解できなくてもClassの習得に影響はありませんが、言語仕様のルーツを知る上で重要なプロセスだと筆者は考えます。

コンストラクタを作成してインタンスに継承

関数をnewをつけて実行するとコンストラクタとして機能します。
生成されたオブジェクトはインスタンスと呼ばれ、コンストラクタのprototypeオブジェクトを参照することが可能になります。

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

Human.prototype.sayName = function() {
    return 'わたしの名前は' + this.name + 'です!';
}

var ymmt = new Human('yt_ymmt');
console.log(ymmt.sayName()); // わたしの名前はyt-ymmtです!

var hoge = new Human('hoge');
console.log(hoge.sayName()); // わたしの名前はhogeです!

object.create()でコンストラクタを継承

object.create()はES5で実装された機能です。
第一引数に指定したオブジェクトをprototypeプロパティに設定した新しいオブジェクトを作ることが可能です。
次のコードでは、HumanクラスをUserクラスに継承しています。

var Human = function(name) {
    this.name = name;
}

Human.prototype.sayName = function() {
    return 'わたしの名前は' + this.name + 'です!';
}

var User = function(name) {
    Human.call(this, name);
}

User.prototype = Object.create(Human.prototype);
User.prototype.constructor = User;

var ymmt = new User('ymmt');
console.log(ymmt.sayName());

User.prototypeにHuman.prototypeを代入することでHuman.prototypeのメソッドを使用可能にしています。その際にconstructorプロパティが上書きされてしまうため代入しなおしています。
ここまでのコードを見て分かる通り、ES5までのクラス生成には以下の問題点が挙げられます。

  • クラス宣言が関数宣言と同じ
  • コンストラクタとプロトタイプの記述が同じコードブロックに記述できない
  • 継承が複雑で工夫が必要になったり、記述も冗長的になる

Class構文ではこれらの問題点が改善されています。

Class構文の書き方

上記でES5までのコードパターンをご紹介させていただきました。継承自体は可能になりますが、複雑になりやすいという欠点が存在していました。 それでは、ここから本題であるClassの説明に移ります。

class宣言

Class構文はまずは「class」と宣言を行い、クラスの名前を記載します。

class className {
}

constructorメソッド

class className {
    constructor(name) {
        this.name = name;
    }
}

クラス内では各メソッドを定義します。constructorメソッドはクラスがnewをされた際に実行されるメソッドで、当然引数もここで受け取ることが可能です。コンストラクタを使用したことがあれば理解は早いでしょう。

メソッド定義

インスタンスに継承したいメソッドもclass構文の中に記述することができます。

class className {
    constructor(name) {
        this.name = name;
    }
    myMethod01() {}
    myMethod02() {}
}

myMethod01myMethod02はClassclassNameが持つメソッドを定義しています。今までprototypeのプロパティとして記述し、インスタンスに継承させていたメソッドはこのようにメソッド名を直接記載して実装できるようになりました。
またconstructorも同じですが、関数宣言文のfunctionも不要となっています。

スタティックメソッド

メソッド名の頭にstaticをつけると定義するメソッドはスタティックメソッドになります。 スタティックメソッドはインスタンスから呼び出すことをせず、クラスを経由して呼び出すメソッドとなります。

class ClassName {
    constructor(name) {
        this.name = name;
    }
    myMethod01() {}
    myMethod02() {}
    static method() {}
}

var child = new ClassName('ymmt');
console.log(ClassName.method()); // 親クラスからアクセスする

スティックメソッドはどんな場合に使用すればいいのか少しわかりづらいため、以下にサンプルコードを書いておきます。
どのような問題点を解決するために実装されたのかを理解するためES5で記載します。

var Human = function(name) {
    this.name = name;
}
Human.isNamed = function() {
    return !!this.name;
}

上記のコードのisNamedメソッドはHumanクラスにnameが設定されているかをチェックするためのメソッドです。 例えばこれを継承先の子クラスに継承したいとすると、非常に難しくなります。
そして実際にHumanクラスに存在するisNamedメソッドを子クラスから実行させようとすると、以下のようなコードになります。

var Human = function(name) {
    this.name = name;
}
Human.isNamed = function() {
    return !!this.name;
}

var User = function(name) {
    Human.call(this, name);
}
User.prototype = Object.create(Human.prototype);
User.prototype.constructor = User;

console.log(User.prototype.__proto__.constructor.isNamed()); // true

isNamedメソッドはprototypeで継承されていないメソッドのため、Userクラスからは実行できません。 そのためprotoを辿って無理やり使用するしかありません。この方法は記述も冗長的な上にオブジェクトのつながりも明確ではないためあまりいいコードではないですね。

この問題を解決するためにES6はスタティックメソッドが実装されています。上の継承をClass構文を使って書きなおしてみます。

class Human {
    constructor(name) {
        this.name = name
    }
    static isNamed() {
        return !!this.name
    }
}

class User extends Human {
    constructor(name) {
        super(name);
    }
    static humanIsNamed() {
        return super.isNamed();
    }
}

console.log(User.humanIsNamed()); // true

まだ解説していない記述がいくつか見られると思います。これは後述いたしますのでスルーしてください。
大事なのは記述が簡素化されている点です。明らかにこちらの記載方法のほうが見通しがよくなり、記述量も減っています。

ゲッターとセッター

メソッド名にgetとsetのどちらかをつけると、ES5のゲッターメソッドとして振る舞うことができるようになります。

class Human {
    constructor(name) {
        this.name = name;
    }
    set name(name) {
        this._name = name.toUpperCase();
    }
    get name() {
        return this._name;
    }
}

var human = new Human('ymmt');
console.log(human.name); // YMMT

インスタンスからゲッターで指定した名前にアクセスした場合の処理を記載することができるようになります。同じようにセッターで指定した名前のプロパティを処理する働きがあった場合の処理を記述することができるようになっています。
上記のコードでは、セッターをnameプロパティにすることで、インスタンス生成時のnameプロパティをセッターでtoUpperCase()の処理をしました。ゲッターで返す際は、this._nameとしてセッターで処理したnameプロパティを返すようにしています。
そもそものsetとgetのついては以下をご参考にしてみてください。

developer.mozilla.org

developer.mozilla.org

エラー処理

Class構文はコンストラクタと比べて、いくつかエラー処理に差異があります。

newが必須

今までのコンストラクタ定義では、通常の関数宣言文としてクラスのような振る舞いをするオブジェクトを生成していました。
そのためnew演算子を使用せずにコンストラクタを実行した場合でも、エラーなどは発生せず通常の関数として処理されてしまう問題がありました。Class構文ではこの問題が解決され、new演算子を付けずに実行した場合、エラーが返るようになっています。

class Human {
    constructor(name) {
        this.name = name;
    }
}
var human = Human('ymmt'); // ここでnewをつけない
// Uncaught TypeError: Class constructor Human cannot be invoked without 'new'

変数の巻き上げが起こらない

functionを使うと、同一スコープ上にあればどこからでも呼び出すことができました。

var test = myFunc(1);

function myFunc(x) {
    return x + 1;
}

しかし、Class構文の場合、先に宣言をしておく必要があります。

var human = new Human(): // TypeError: undefined is not a function

Class Human {};

まとめ

Class構文の基本的な書き方などは以上になります。
長くなってしまうためClassについては2回に分けるようにさせていただきました。
次回は継承まわりを重点的に解説します。