LoginSignup
3
4

More than 5 years have passed since last update.

ES6でのクラスと継承…裏側を見てみる

Last updated at Posted at 2017-06-15

なかなか書きづらいJavaScriptでのオブジェクト指向(特に、継承について)でしたが、ES6ではついに公式なクラス・継承機能が登場しています。

ES6のクラスについて

とはいえ、プロトタイプベースという言語の本質はES6でも何ら変わらず、クラス構文と言っても中身はプロトタイプベースで動きます。

逆を返せば、「比較的容易に、従来のプロトタイプベースなコードに変換できる」「従来のコンストラクタ&prototypeで書いた『クラス』を、class構文で継承できる」などの融通無碍さはそのままです。

Babelでの翻訳例

最近のブラウザでは直接class構文を投げても動くのですが、IE11を考えれば少なくともあと数年は、ブラウザ向けのJavaScriptに直接書けない状況が続きます。ということで、Babelなどのトランスパイラを使って、ES5に変換して使うこととなります(なお、同じように動けばいいので、変換後の関数名などは違うかもしれません)。

// ES6でのclassを使ったコード
class Test{
  constructor(){
    this.foo();
  }

  foo(){
    alert('Test::foo()');
  }
}

class Child extends Test{
  foo() {
    alert('Child::foo()');
  }
}
// ES5へのトランスパイル後

// ※この3関数は、読みやすさのため途中改行を入れています
function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, enumerable: false, writable: true, configurable: true }
  });
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Test = function () {
  function Test() {
    _classCallCheck(this, Test);

    this.foo();
  }

  Test.prototype.foo = function foo() {
    alert('Test::foo()');
  };

  return Test;
}();

var Child = function (_Test) {
  _inherits(Child, _Test);

  function Child() {
    _classCallCheck(this, Child);

    return _possibleConstructorReturn(this, _Test.apply(this, arguments));
  }

  Child.prototype.foo = function foo() {
    alert('Child::foo()');
  };

  return Child;
}(Test);

class構文

まず下の方にあるTestの定義を見てもらいたいのですが、基底クラスのないclass構文では、ほぼ「constructor→コンストラクタとなる関数」、「インスタンスメソッド→prototypeのメソッド」と置き換えているだけです1

_classCallCheckが入っていますが、これは「ES6 classをnewなしで呼ぶとTypeErrorになる」ので、それを再現するためのものです。

継承

ES5の鬼門だった継承は、どのようになっているでしょうか。これは_inherits関数で実現されています。中身としては、

  1. 親クラスがnullもしくは関数かをチェック→違えばTypeError
  2. 親クラスのプロトタイプを引き継いで、そしてconstructorも正しく設定したオブジェクトを子クラスのprototypeに設定
  3. 子クラスの__proto__も親クラスに設定2

そして、親クラスのコンストラクタをapplyで呼んでいますが、instanceofはプロトタイプを遡るので、問題なく
_classCallCheckをクリアできます。_possibleConstructorReturnですが、「newで呼ばれた関数は、内部でオブジェクトをreturnした場合は、新しく生成したオブジェクトの代わりにそれを返す」ということになっているので、その挙動を取り入れるためのものです。

なお、superで親クラスのメソッドを呼ぶと、親クラス.prototype.method.callのような形にコンパイルされます。そして、メソッド内のthisthisのままなので、親クラスの中で呼んだメソッドも、そのオブジェクト自身で解決されます(子クラスやオブジェクト自体のメソッドがあれば、そっち優先)。

まとめ

ES6の実装前には、継承の方法もライブラリや環境によって違うようなカオス状態でしたが、ようやく統一された形となって一件落着した気もします。


  1. 性質上、ES6時点でコンストラクタがなくても、_classCallCheckを呼ぶだけのコンストラクタが自動生成されます。 

  2. これは、クラスオブジェクト自体(staticメソッド)の継承のためで、インスタンスメソッドには影響しません。 

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4