JS.next

JavaScriptの最新実装情報を追うブログ

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(デフォルト有効)