なかなか書きづらい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
関数で実現されています。中身としては、
- 親クラスが
null
もしくは関数かをチェック→違えばTypeError
- 親クラスのプロトタイプを引き継いで、そして
constructor
も正しく設定したオブジェクトを子クラスのprototype
に設定 - 子クラスの
__proto__
も親クラスに設定2
そして、親クラスのコンストラクタをapply
で呼んでいますが、instanceof
はプロトタイプを遡るので、問題なく
_classCallCheck
をクリアできます。_possibleConstructorReturn
ですが、「new
で呼ばれた関数は、内部でオブジェクトをreturn
した場合は、新しく生成したオブジェクトの代わりにそれを返す」ということになっているので、その挙動を取り入れるためのものです。
なお、super
で親クラスのメソッドを呼ぶと、親クラス.prototype.method.call
のような形にコンパイルされます。そして、メソッド内のthis
はthis
のままなので、親クラスの中で呼んだメソッドも、そのオブジェクト自身で解決されます(子クラスやオブジェクト自体のメソッドがあれば、そっち優先)。
まとめ
ES6の実装前には、継承の方法もライブラリや環境によって違うようなカオス状態でしたが、ようやく統一された形となって一件落着した気もします。