HTML5Experts.jp

JavaScriptにもクラスがやってきた!JavaScriptの新しいclass構文をマスターしよう

ECMAScript 2015(ECMAScript 6)で新たに追加された待望のclass構文について、その概要をサンプルコードを交えて紹介します。

これまでのJavaScriptにおけるクラス

多くのプログラミング言語はクラスを作れる機能を持っていますが、JavaScriptにその機能は用意されていませんでした。しかし、JavaScriptにはprototypeという柔軟な仕組みが存在しており、このprototypeを利用することで、他の言語で表現されている「クラス」と似たような振る舞いを再現することが可能でした。

それは例えば、こんなふうにです。

/* Cat雛形の作成 */

function Cat(name) { this.setName(name); }; Cat.prototype = { setName: function(name) { this._name = name; }, getName: function() { return this._name; }, walk: function() { console.log(this._name + 'が歩いてます'); } };

/* Catからオブジェクトを作成 */

var cat1 = new Cat('タマ'); var cat2 = new Cat('コタロー'); cat1.walk(); // タマが歩いています cat2.walk(); // コタローが歩いています

コンストラクタであるfunction Catを宣言し、そのprototypeにインスタンスメソッド群を定義したオブジェクトを指定します。すると、new Catした時、作成されたオブジェクトのプロパティは、そのオブジェクト自身に直接定義されていない場合、Cat.prototypeに定義されている同名のプロパティを参照するという具合です。

このように、単純なオブジェクトの雛形を作るだけならまだよいのですが、あるクラスを継承し、別のクラスを作る、いわゆる「クラスの継承」を表現するのはちょっと複雑です。その実装方法についてはここでは割愛しますが、例えばこのCatであれば、より上位の概念を表現したクラスAnimalを用意し、CatAnimalを継承して作られる……というような場合です。複雑と言っても、よく使われる概念ではあるので、これまでは多くのライブラリやトランスパイラがこの機能を担っていました。

このような、prototypeを利用した「クラス」的な振る舞いの実装は、JavaScript初学者のひとつのハードルになっていたと言ってもよいのではないでしょうか。

ES6のclass構文

そんな「クラス」を実現するclass構文が、ES6では用意されました。

class構文を使えば、クラスの継承も簡単に行えます。その仕様の実態は、prototypeベースの継承であり、単なる糖衣構文に過ぎませんが、複雑な実装をすることなく誰でも簡単にクラスを扱えるようになったことは大きな意味があると言ってよいでしょう。

先ほどのCatを、ES6のclass構文で書くと、以下のようになります。

/* Catクラス */

class Cat { constructor(name) { this.name = name; } set name(name) { this._name = name; } get name() { return this._name; } walk() { console.log(this._name + 'が歩いてます'); } }

/* Catクラスのインスタンス作成 */

var cat1 = new Cat('タマ'); var cat2 = new Cat('コタロー'); cat1.walk(); // タマが歩いています cat2.walk(); // コタローが歩いています

これで先ほどと同様の実行結果になります。

※ ここで使っているsetはsetter、getはgetterとしてES5で定義されている構文、walkメソッドを定義しているのはMethod definitionで、ES6で定義されている構文です。class構文とはまた別なので注意して下さい。

静的メソッド

class構文は、静的メソッド※の定義方法も用意しています。先ほどのCatに、作成した猫の数を返すcountメソッドを実装してみたのが以下です。

※クラスのインスタンスを作らずとも呼べるメソッドのことを言います。

var catCount = 0; // 作った猫の数

/* Catクラス */

class Cat { constructor(name) { this.name = name; catCount += 1; // 作った猫の数を+1 } set name(name) { this._name = name; } get name() { return this._name; } walk() { console.log(this._name + 'が歩いてます'); } /* 静的メソッド */ static count() { return catCount; } }

/* Catクラスのインスタンス作成 */

console.log(Cat.count()); // 0 var cat1 = new Cat('タマ'); console.log(Cat.count()); // 1 var cat2 = new Cat('コタロー'); console.log(Cat.count()); // 2

Catconstructorが実行されると猫の匹数を示すcatCountがインクリメントされ、静的メソッドのcount()が呼ばれると、このcatCountを返します。

※ ちなみに、メソッド以外の静的なプロパティを設定すること(ここだと例えばCat.catCountに猫の数を格納するようなこと)は、class構文には今のところ用意されていないようです。これについては2015年10月現在、proposalが出ているだけの状態で、今後どうなるかはまだ分かりません。

クラスの継承

クラスを継承し、別のクラスを作るには、extendsを使います。以下は、Animalを継承したCatクラスを定義した例です。

/* Animalクラス */

class Animal { constructor(name) { this.name = name; } set name(name) { this._name = name; } get name() { return this._name; } walk() { console.log(this.name + 'が歩いてます'); } }

/* Catクラス */

class Cat extends Animal { // Animalを継承したCatクラスを定義 mew() { console.log(this.name + 'がニャーと鳴いてます'); } }

/* Catクラスのインスタンス作成 */

var cat1 = new Cat('タマ'); console.log(cat1.name); // タマ cat1.walk(); // タマが歩いてます cat1.mew(); // タマがニャーと鳴いてます

var cat2 = new Cat('コタロー'); console.log(cat2.name); // コタロー cat2.walk(); // コタローが歩いてます cat2.mew(); // コタローがニャーと鳴いてます

Catのインスタンスオブジェクトであるcat1cat2は、Catに定義されているメソッドが利用できるだけではなく、Animalに定義されているconstructorで初期化され、Animalで定義されているメソッドを利用できていることが分かります。

継承元のメソッド呼び出し

クラスを継承した場合、継承元のメソッドを呼びたいことがあります。その場合、以下のようにsuperを使います。

/* Catクラス */

class Cat extends Animal { constructor(name, color) { super(name); // Animalクラスのconstructor呼び出し this.color = color; } get color() { return this._color; } set color(color) { this._color = color; } mew() { console.log(this.color + '色の' + this.name + 'がニャーと鳴いてます'); } walk() { super.walk(); // Animalクラスのwalkメソッド呼び出し console.log('忍び足です'); } }

/* Catクラスのインスタンス作成 */

var cat1 = new Cat('タマ', '茶'); console.log(cat1.name); // タマ cat1.walk(); // タマが歩いてます // 忍び足です cat1.mew(); // 茶色のタマがニャーと鳴いてます

var cat2 = new Cat('コタロー', '黒'); console.log(cat2.name); // コタロー cat2.walk(); // コタローが歩いてます // 忍び足です cat2.mew(); // 黒色のコタローがニャーと鳴いてます

constructor内で、super(name)と、Animalconstructorを実行し、walk()内でsuper.walk()と、Animalwalkを実行しています。

class構文を使わない場合、Animal.prototype.walk.apply(this, arguments)などと、継承元のメソッドをprototype経由で直接参照し、applyでコンテキストを自分にして実行するなどというような実装が必要です。superを使えば、同じ内容をとてもシンプルに書くことができます。

その他注意点

他、筆者がclass構文を使ってみて気になったこととして、インスタンスのプロパティをprototype直下に直接指定できないという点が気になりました。例えば、はじめに挙げたCatでインスタンス作成時にnameを受け取らない場合、以下のように書けば、setNameする前にgetNameが呼ばれた場合、'名無しの猫'が名前として返ります。

/* Cat雛形の作成 */

function Cat() {}; Cat.prototype = { _name: '名無しの猫', /* _nameの初期値として指定 */ setName: function(name) { this._name = name; }, getName: function() { return this._name; }, walk: function() { console.log(this._name + 'が歩いてます'); } };

しかし、ES6のclass構文では、class宣言時に設定できるのはメソッドのみです。上記例のように_nameprototypeに指定することができません。インスタンスのプロパティの初期値を設定したい場合、constructor内で設定する必要があります。

prototype直下にオブジェクトを指定した場合、全てのインスタンスで同一のオブジェクトを参照してしまい、思わぬバグの原因になったりすることもありえるため、このような書き方をそもそもしないほうがよいという見解もあります。

ブラウザ対応状況

class構文の対応状況は、2015年10月現在、まだまだといった状況です。

筆者が確認したタイミングで対応しているのは、Safari9 (Webkit)だけです。デベロッパー版や設定フラグなど考慮すると Chrome45 (enable-javascript-harmonyをオンにし、strict modeのみで動作)、Firefox nightly (v44)、Microsoft Edge(experimental featureをオンにして使用)などで利用できます。一般的な閲覧環境を想定する場合、class構文を使用するのはまだ先だと考えておいてよいでしょう。bableやtraceur等のトランスパイラは対応しています。ただし、まだclass構文の全ての機能に対応しているわけではないようです。

ちなみに筆者は、本稿を書くためのサンプルを、Chrome canaryで動作することを確認しています。

まとめ

以上、ES6のclass構文について、その概要を簡単に紹介しました。筆者は、好きでよくCoffeeScriptを使っていたのですが、CoffeeScriptを使う大きな理由の一つとして、CoffeeScript独自のclass構文が使えるようになっているというところがありました。そういったコードの設計の根幹部分が言語として用意された状態だと、コードの見通しがよくなるなぁと感じていまして。しかし、ES6にてclass構文が実装されたとなれば、自分がCoffeeScriptを選択する理由が一つ、なくなった感じがします。

ほか、class構文を使って書かれたコードは、そのコードで表現したいことが、同様の内容をclass構文を使わないで書いた場合よりも、よりシンプルに表現できているように感じます。これは、チーム間でのコミュニケーション効率を高め、開発をスムーズにするかもしれません。

class構文がやってくれることは、これまでJavaScriptで書かれてきたいわゆる「クラス」的なものの実装そのものであるため、class構文を使ったからといって、これまで書いてきたJavaScriptと根本的に設計が変わってしまうということはないでしょう。ブラウザの対応はまだまだのようですが、babelをお供に、開発に採用できるケースはかなり多いのではないかと思います。