uhyohyo.net

JavaScript初級者から中級者になろう

十六章第十一回 クラス

このページの最終更新日:

ES2015の目玉機能のひとつがクラスです。JavaScriptにおけるクラスは、端的にいえばコンストラクタを作る新しい方法です。まず簡単な具体例から見ましょう。


class Foo{
  constructor(arg){
    this.prop = arg;
  }
}

var obj = new Foo(100);
console.log(obj); // Foo {prop: 100}

1行目から5行目が新しい構文、クラス宣言です。このように、class クラス名{ ... }の形でクラスを作ることができます。

ただし、JavaScriptにおいてクラスという概念は本来存在しません。第九章で見たように、その実体は関数(コンストラクタ)です。実際、typeof演算子を使って調べるとtypeof Foo === "function"となるはずです。つまり、クラス宣言によって作られる“クラス”は関数なのです。

ただし、クラスとして作られた関数はコンストラクタ専用なので、newをつけずに呼び出すことはできません。この章では「newでは呼び出せない関数」が何回か登場しましたが、その逆というわけですね。関数の役割の明文化の一環です。

クラス宣言の中身は、実はオブジェクトリテラル内の関数の省略記法と同じ記法を使います。つまり、上のクラス宣言はconstructorという関数が定義されているものとして読むことができます。

ただし、constructorという名前の関数は特別扱いとなり、この関数の処理がコンストラクタの処理となります。よって、上の例でnew Foo(100)とされた場合、上で定義されたconstructor関数が呼ばれます。そこでthis.prop = arg;が実行されたので、new Foo(100)の結果であるobjオブジェクトはプロパティpropに100が入っています。コンストラクタの中では、thisは当然今作られているオブジェクトです。

メソッドの定義

昔の方法では、メソッドはprototypeを用いて定義しました。クラスの構文を使うともっと楽に定義することができます。上ではconstructorという特殊な関数のみクラス内で定義しましたが、他の関数も定義することができます。そのようにして定義された関数がそのクラスのメソッドとなるのです。


class Teki{
  constructor(name){
    this.name = name;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

var zako = new Teki("ザコ");
zako.attack();

なんだか懐かしいサンプルですね。この例ではTekiオブジェクトにattackというメソッドが付きました。

なお、裏で行われていることは基本的にこれまでと同じです。attackメソッドの実体はTeki.prototype.attackにあります。よって、例えばzako.attack === Teki.prototype.attackはtrueになります。

ただし、enumerable属性はfalseに設定されます。

つまるところ、クラス宣言は我々が第九章で四苦八苦したことを簡単にやってくれる便利な構文だと理解するのがよいでしょう。

staticメソッド

クラス宣言の中でメソッドを定義するときは、定義の前にstaticと書くことによりstatic関数を定義することができます。


class Teki{
  constructor(name){
    this.name = name;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
  static makeZako(){
    return new Teki("ザコ");
  }
}

var zako = Teki.makeZako();
zako.attack();

違いは単純であり、staticが付いた定義はTeki.prototypeではなくTekiのメソッドとして定義されます。すなわち、staticな定義はインスタンスのメソッドを定義するのではなく、コンストラクタにくっついたメソッドを定義します。当然ながら、この例ではzako.makeZako()とはできません。

この機能は、コンストラクタのメソッド(例えばArray.fromとか)を作りたいときに役に立ちます。

クラスの基本的な機能は(現在のところ)これで終わりです。言語によってはクラスに「privateメソッド」とかいう機能が備わっていますが、それはありません。(ただし、クラス宣言におけるプロパティ定義構文がStage 3、privateメソッド/プロパティはStage 2ですので(2017年10月現在)、ES2018くらいにこれらの機能が追加されるかもしれません。)

ただし、クラスのもう1つの重要な機能である継承がまだ残っています。これは次回に紹介するとして、今回はやや細かい話をしておきます。

クラス宣言のスコープ


{
  class Foo{
  }

  var foo = new Foo();
  console.log(foo); // Foo {}
}

console.log(typeof Foo); // "undefined"

この例から類推されるように、クラス宣言は任意のブロックの中に書くことができます。このとき、それにより定義されたクラスはそのブロックをスコープとして持ちます。すなわち、ここで定義されたクラスFooはこのブロックをスコープとするローカル変数として存在しているということです。

余談ですが、クラスの定義はこのようにconstructor関数を省略することができます。その場合、コンストラクタは何もしない関数になります。

クラス式

今まではクラス宣言を説明しましたが、クラス式もあります。関数宣言に対する関数式みたいですね。これは言わずもがな、その場でクラスを生成して返す式です。


var Foo = class {
  hello(){
    console.log("Hi!");
  }
};

var obj = new Foo();
obj.hello(); // "Hi!"

関数式の場合と同様、クラス式の場合はクラス名を省略できます。

コンストラクタの返り値

コンストラクタは関数なので、返り値を返すこともできます。この場合、実はnewでインスタンスを作った結果がその返り値の値になります。


class Foo {
  constructor(x, y){
    return [x, y];
  }
}

var arr = new Foo(1, 2);

console.log(arr); // [1, 2]
console.log(arr instanceof Foo); // false

この例では、クラスFooのコンストラクタは配列を作って返します。すると、new Foo(1, 2)というnew呼び出しの結果が配列になりました。これは本当にただの配列であり、Fooのインスタンスではありません。こうなるとFooクラスを作った意味があまり無いような気がします。

今回の話はこれで終わりですが、次回も引き続きクラスの話です。

クラスの構文は分かりやすく便利なので、オブジェクト指向的なことをするときはぜひ使っていきましょう。