Class構文について
概要
待ち焦がれた人も多いことだろう。ES2015の一番の目玉機能とも言えるクラス構文が、ついにV8でサポートされた。
Class構文は、『関数(コンストラクタ)定義』+『.prototypeへのメソッド定義』の糖衣構文である。
JSで今まで様々に工夫されてきたクラスの書き方を、綺麗に統一してくれる可能性を秘めている。
クラスを作る
従来、Catクラスを作ろうとした場合このように書いてきた。
function Cat(name) { this.name = name } Cat.prototype.meow = function () { alert( this.name + 'はミャオと鳴きました' ) }
しかしこの書き方だとどうしても、コンストラクタとメソッドの定義が分離されているため、クラスとしてまとまりがなく分かりづらく感じる。
メソッドが増えてきた時も、Cat.prototypeにオブジェクトを代入する形で定義をまとめようとした場合に、
デフォルトで定義されていたCat.prototype.constructorの存在等、留意しなければならない事項も存在する。
やはり、クラスはクラスとして素直にスッキリ定義したい。そんな希望に答えてくれるのがClass構文である。
先ほどの例をClass構文を用いるとこう書ける。
class Cat { constructor(name) { this.name = name } meow() { alert( this.name + 'はミャオと鳴きました' ) } }
Class構文はまず「class」と書き、その後にクラス名、メソッド定義ブロックと続く。
メソッド定義ブロックは、オブジェクトリテラルでのメソッド短縮定義に似た記法で書く。
ただしメソッドを区切るコンマは置かず、代わりにセミコロンを置いてもよい。
Class構文の振る舞いとしては、まずメソッド定義ブロック中の"constructor"メソッドが特別にクラス名で関数定義され、その関数のprototypeオブジェクトに全てのメソッドが定義される形になる。
つまりclass構文を使っているが、依然「typeof Cat === "function"」であり、あくまで従来型の書き方をスッキリ書ける、糖衣構文に過ぎないという点は注意である。けして新しい概念が入るわけではない。
継承する
従来型ではクラスの継承関係を作ろうと思うと大変だった。
例えば次のようなAnimalクラスを作り、
function Animal(name) { this.name = name } Animal.prototype.speak = function (cry) { alert( this.name+ 'は' +cry+ 'と鳴きました' ) }
それを継承したCatクラスを作ろうとすると、このように工夫や冗長な記述が必要だった。
function Cat(name) { Animal.call( this, name ) // 冗長 } Cat.prototype = Object.create( Animal.prototype ) // 工夫 Cat.prototype.constructor = Cat // 工夫 Cat.prototype.meow = function () { Animal.prototype.speak.call( this, 'ミャオ' ) // 冗長 }
これでなんとか上手くいっているようにも見えるが、まだ問題がある。
Animalクラスに、名前が定義されているかどうかの真偽値を返す、isNamedスタティックメソッドを定義したい場合、
Animal.isNamed = function () { return !!this.name }
これもCatクラスに継承させたいときが困る。Animal関数をCat関数のプロトタイプにできればいいのだが、
ES5の時代まではブラウザ毎に独自実装されていた「__proto__」を用いない限り無理であった。
そこで、Class構文を用いるとまずAnimalクラスはこう書ける。
class Animal { constructor(name) { this.name = name } speak(cry) { alert( this.name+ 'は' +cry+ 'と鳴きました' ) } static isNamed(animal) { return !!animal.name } }
スタティックメソッドを定義する際は、メソッド名の前に「static」修飾子が付くのがポイントである。
そして、それを継承したいCatクラスはこのように書けばいい。
class Cat extends Animal { constructor(name) { Animal.call( this, name ) } meow() { Animal.prototype.speak.call( this, 'ミャオ' ) } }
クラス名の後に、「extends」そして『親クラス』を指定するだけで、親クラスを継承したクラスを定義することができる。
そうすると振る舞いとしては、Animal.prototypeをCat.prototypeのプロトタイプとするだけではなく、Animal関数をCat関数のプロトタイプにすることもやってくれる。
つまり「Cat.isNamed(cat)」とできるということである。
更に上の例はsuperキーワードを使ってこう書くことができる。
(実際にはextendsしたクラスのコンスタラクタではthisが最初から初期化されていないため、superでthisを初期化せずに上記例のように書くことはできないし、クラスに対して.callを適用することも出来ない。従って下記のように書くこととなる。)
class Cat extends Animal { constructor(name) { super( name ) } meow() { super.speak( 'ミャオ' ) } }
従来型との対比
Class構文の糖衣構文性を示すために、Point2DとPoint3Dというクラスを、新旧の書き方で並べて比較してみる。
function Point2D(x, y) { this.x = x this.y = y } Point2D.equal = function (p, q) { return p.x == q.x && p.y == q.y } Point2D.prototype = { constructor: Point2D, get length() { return Math.hypot(this.x, this.y) }, }
class Point2D { constructor(x, y) { this.x = x this.y = y } static equal(p, q) { return p.x == q.x && p.y == q.y } get length() { return Math.hypot(this.x, this.y) } }
Point3D.__proto__ = Point2D function Point3D(x, y, z) { Point2D.call(this, x, y) this.z = z } Point3D.equal = function (p, q) { return Point2D.equal.call(this, p, q) && p.z == q.z } Point3D.prototype = { __proto__: Point2D.prototype, constructor: Point3D, get length() { return Math.hypot(this.x, this.y, this.z) }, }
class Point3D extends Point2D { constructor(x, y, z) { super(x, y) this.z = z } static equal(p, q) { return super.equal(p, q) && p.z == q.z } get length() { return Math.hypot(this.x, this.y, this.z) } }
その他の特徴
匿名クラス
関数のように、宣言するだけではなく、値として変数に代入することもできる。
その歳にクラス名を書かずに匿名クラスにすることも可能である。
例:
var C = class { }
not callable
クラスはnew無しで呼ぶ事はできない。
例:
var C = class { } C() // TypeError
strict
Class内は常にstrict modeである。
例:
(class { constructor() { return this } })() // undefined not window
列挙
Classのメソッドは列挙されない。 例:
Object.getOwnPropertyDescriptor( class { static m(){} }, 'm' ).enumerable // false
constructor定義を省略したクラス
extendsがないクラスでは
constructor() { }
という何もしないコンストラクタが補われ、
extendsがあるクラスでは
constructor(...args) { super(...args); }
という、親クラスのコンストラクタに全引数をそのまま渡し、ただしthisは自クラスのprototypeオブジェクトをプロトタイプとしたオブジェクトとして、処理してもらうコードが補われる。
つまり、上の方で挙げたAnimalクラスを継承するCatクラスのコードは更に短縮してこう書くことが出来る。
class Cat extends Animal { meow() { super.speak( 'ミャオ' ) } }
継承について
「extends」の後には継承するクラスを指定するのだったが、クラスとはつまり関数(+付随するメソッド)であるので、
「extends」の後に従来型のクラスを指定することも当然可能であるし、逆に従来型(+「__proto__」)の手法ででクラスを継承することもできる。
例えば、『従来型との対比』の項のサンプルコードは、上下斜めの組み合わせでもきちんと動作する。
また、このような記述も可能である。
var c = new class extends class extends function (x) { this.x = x } { constructor(x) { super(x * 3) } } { constructor(x) { super(x + 2) } }(5) console.log(c.x) // 21
プロパティを定義したい時
万能に見えるClass構文だが、メソッドでないプロパティを定義することは現状できない。
そうしたい場合は構文の外で改めて代入するか、アクセサで代用する。
例:改めて代入
class C { } C.className = 'C' C.prototype.class = C
例:アクセサ
class C { static get className() { return 'C' } get class() { return C } }
巻き上げは起こらない
functionを使った関数宣言と違い、classを用いて宣言すると巻き上げが起こらない。
var c = new C // Error class C { }
実装されるバージョン
V8 ~3.30.21(基本) 3.30.22(+super) -3.31.7-(バグfix) 3.31.58-3.32.x(デフォルト有効) -4.2-(最新の仕様に追従中)
Chrome 42(デフォルト有効)