memolu

いろいろメモってます

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

本連載について

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

前回のおさらい

ES5までの間、クラスを実装しようとした動きはありましたが完璧ではない部分もあり、コードの見通しや設計の観点から見てもやや不自由な点がありました。それに対してのアプローチとしていくつか例を挙げて説明させていただきました。
下のコードはES5時点での簡単なコンストラクタです。

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

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

これだけですとシンプルですが下に挙げる問題点がありました。

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

上記をClass構文で書くと次のようになります。

class Human {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        return this.name;
    }
}

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

コードブロックも一つになり、関数宣言との差別化もできています。継承部分もとても簡略化されているため、今までのような工夫を行う必要はなくなりました。今回はこの継承まわりを説明したいと思います。

ES5までの継承

プロトタイプチェーン

まずはES5までの継承を説明していきます。
次のコードはプロトタイプチェーンを使用した今までの継承方法です。ES5でObject.create()が実装され、ある程度楽になりましたが、根本的な解決には至っておらず、見て分かる通りある程度工夫が必要になっています。
次のコードでは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;

User.prototype.speak = function() {
    return Human.prototype.sayName.call(this);
}

var user = new User('ymmt');
console.log(user.speak()); // ymmt

このコードは以下のような図で表されます。

---------      -------------------
| Human | ---> | Human.prototype |
---------      -------------------
                       ↑
--------      ------------------
| User | ---> | User.prototype |
--------      ------------------
   ↓                   ↑
--------               |
| user | --------------┘
--------

コンストラクタから生成されるインスタンスは自分自身にプロパティを保持しておらず、prototypeオブジェクトを通じてメソッド呼び出す仕組みでした。このprototypeオブジェクトをクラス間で繋げることで継承関係を実装するのが今までの基本です。 上記のコードでも動作自体に問題はないのですが、以下に挙げる部分が分かりづらくなります。

  • Userクラスに継承するにはコンストラクタでHumanクラスをcall()して呼び出す必要がある
  • UserクラスのprototypeをHumanクラスのプロトタイプを代入する必要がある
  • Userクラスのprototype内にあるconstructorプロパティをUserに書き換える必要がある
  • Humanクラスのprototype上メソッドをUserクラスのインスタンスから呼び出す際に冗長的な記述になる

この短いコードでもこれだけの問題点が挙げられます。さらにObject.createを使用しない場合などはさらに処理が複雑になる問題もありました。 そしてそのような複雑な箇所はプラグインフレームワークなどで解決されており、プロジェクトごとに対応方法に差異がある問題もありました。 Class構文はjavascriptの言語仕様として今回実装されたため、このような問題の解決に期待ができます。

Class構文での継承

Class構文での継承を行う場合、extendsを使用します。

class foo extends bar {}

上に記載したコードをClass構文に置き換えてみましょう。

class Human {
    constructor(name) {
        this.name = name
    }
    sayName() {
        return this.name;
    }
}

class User extends Human {
    constructor(name) {
        super(name);
    }
    speak() {
        return super.sayName();
    }
}

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

Humanクラスは継承されるクラスのためclassを使用して宣言すれば問題ありません。UserクラスはHumanクラスを継承させる必要があるため、extendsを宣言し、Humanクラスを記載しておきます。
これだけで継承することが可能になっています。

継承元のコンストラクタを呼び出す

上記コードの説明を続けます。以下の部分に注目してください。

constructor(name) {
    super(name);
}

superを使うと継承元のコンストラクタやプロパティにアクセスして、メソッドも実行することができます。super()にすると継承元クラスのコンストラクタを実行するこができるようになります。

---------
| Human |
---------
   ↑ super(name)
--------
| User |
--------

継承元のメソッドを呼び出す

上述の通り、superを使用することで継承元クラスのメソッドも呼び出すことが可能です。

speak() {
    return super.sayName();
}

今まで通りprototypeを使って継承元クラスにアクセスすることも可能ですが、superを使用することで記述をより簡単に書くことができるようになりました。

継承元のスタティックメソッドを使う

上ではprototype上にあるメソッドを実行していました。次のコードは継承元クラスのスタティックメソッドを呼び出す方法です。
といっても特に難しいこともなく同じsuperを使って呼び出すことが可能です。

class Human {
    constructor(name) {
        this.name = name
    }
    static foo() {
        return 'foo';
    }
}

class User extends Human {
    constructor(name) {
        super(name);
    }
    static bar() {
        return super.foo();
    }
}
console.log(User.bar()); // foo

このように継承元であるHumanクラスのスタティックメソッドfooをUserクラスのスタティックメソッドbarからsuper経由で呼び出して実行しています。
Userクラスのスタティックメソッドbarをスタティックメソッドにしないとエラーが発生して実行できない点には注意してください。

// Userクラスのbarをスタティックメソッドにしないとエラー
class Human {
    constructor(name) {
        this.name = name
    }
    static foo() {
        return 'foo';
    }
}

class User extends Human {
    constructor(name) {
        super(name);
    }
    bar() {
        return super.foo();
    }
}
console.log(User.bar()); // (intermediate value).foo is not a function

継承元のゲッター・セッターを使う

継承元のゲッターやセッターにもsuperクラスを使用してアクセスすることが可能です。
次のコードではUserクラスのnameプロパティを継承元のHumanクラスのゲッター・セッターを呼んで処理しています。

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

class User extends Human {
    set name(name) {
        super.name = name;
    }
    get name() {
        return super.name;
    }
}

var user = new User();
user.name = 'ymmt';
console.log(user.name); // YMMT

余分な処理を挟まないようにconstructorは省略しています。
setではsuper.name=nameのようにして、継承元のnameプロパティにアクセスするようにします。そうすることで、継承元クラスのnameプロパティが処理されtoUpperCase()まで実行されます。
getではreturnに継承元クラスのプロパテイを記載するだけです。これでtoUpperCase()まで処理された値が返るようになり、YMMTがreturnされています。

まとめ

これで継承方法についての説明も完了です。
Classは少し長くなってしまうため2回に分けて解説しました。しかし今までのjavascriptにおける継承方法をsuperという大変便利なプロパティからアクセスできるようになり、コードの記述量と可読性が大幅に向上されました。
Class構文自体もコンストラクタとプロトタイプをカプセル化することに成功しており、各メソッドも分散しづらくなり、見通しがよくなっています。
もしご自身のプロジェクトでES6が使用できる環境であれば、まず使わない理由がないほどの機能のため、ぜひ活用していただければと思います。