ES2015新機能: JavaScriptのclassとmethod

  • 50
    いいね
  • 0
    コメント

Koaの勉強をしたいのだけれど、こいつはES2015を前提にしているので、そこで入ってきた機能を一旦おさらいしようと思い、前にやってた宣言やarrow関数の続きみたいな感じで、クラスとメソッドの新しい書き方について見ようかなと思いました

メソッド

新しいメソッドの書き方

ES2015だと、メソッドの書き方も新しく追加されているようです。この前のarrow関数含め、メソッドの定義方法には3種類あることになります。

'use strict'

let obj = {
  test1: function(x) {return typeof x},
  test2: x => typeof x,
  test3(x) {return typeof x}
};

console.log(obj.test1(1));//number
console.log(obj.test2(2));//number
console.log(obj.test3(3));//number

ひとつ目のtest1は従来のメソッドの書き方になります。オブジェクトのプロパティに無名関数を入れる形式ですね。
ふたつ目はarrow関数を使って書いたものです。これは前にやりました
みっつ目がES2015で追加されたメソッドの新しい書き方です。
arrow関数含めて、ES2015ではメソッドの書き方が2つも増えたようです

メソッド内部のthis

arrow関数はthisが字句解析の時点のスコープに固定されていましたが、もう一つの記法はどうでしょうか

'use strict'

let obj = {
  test1: function() {return typeof this},
  test2: () => typeof this,
  test3(x) {return typeof this}
};

console.log(obj.test1.call(1));//number
console.log(obj.test2.call(2));//object
console.log(obj.test3.call(3));//number

こんな感じになりました。
callは関数の呼び出し元を指定して実行できるメソッドで、上記のコードでは、number オブジェクトに呼び出されたとして、各テストメソッドを実行していることになります。
結果としては、やはりarrow関数だけは呼び出し元に左右されることなく、thisがその定義されているオブジェクト自身を指ししています。
一方で、通常の無名関数記法と、新メソッド記法ではthisが呼び出し元(ここではnumberオブジェクトの1および3)を指し示しています。

私の場合、thisが呼び出し元によって変わるとかは、なんともしっくりこない挙動なので、なるべくならarrow関数を使っていきたいところです。

クラス

クラスの基本記法

クラスの基本的な書き方は以下の様なコードになるようです。

'use strict'

// 従来の関数でのクラス定義
function Test1(say) {
  this.greet = say;
}

Test1.prototype.sayHello = function() {//arrow関数はダメ
  console.log(this.greet);
}

// 新しいクラス構文を使ったクラス定義
class Test2 {
  constructor(say) {
    this.greet = say;
  }

  sayHello() {
    console.log(this.greet);
  }
}

const test1 = new Test1('Hello!!');
const test2 = new Test2('Good Morning!!');

test1.sayHello();//Hello!!
test2.sayHello();//Good Morning!!

まず、関数でクラス(みたいなもの)を定義する方法をおさらいすると以下のとおりです

  1. まず普通に関数を定義する。この関数がクラスにおけるコンストラクタと同様のものとなる
  2. 定義した関数のprototypeにメソッドもしくはプロパティを定義する
  3. new 演算子を使用して関数を呼び出す。このとき、関数は空のオブジェクト({})によって呼び出されたこととなり、関数の処理終了時にそのオブジェクトが返却される

こんな感じです。ちなみに、コードの中でprototypeに対してはarrow関数で定義していません。ここで使用されているthisが字句解析の時点ではundefinedオブジェクトになってしまうからです。
一方、クラス構文の方は以下の様な書き方です

  1. classキーワードでクラス名を定義する
  2. コンストラクタメソッドを定義する
  3. その他のメソッドを定義する

こんな感じです。
メソッドの定義方法は、先に紹介したメソッドの新しい書き方を使用しています。

クラスの持つ機能

継承

「class」が使えるのであれば、継承も使えるのが筋です。
よくある例で、「動物」→「哺乳類」→「ネコ科」→「三毛猫」みたいな感じで、より大きな特徴でまとめながら、細かく分類していくことで、一つ一つのクラスが持つ特徴量を少なくし、見通しをはっきりさせることができます

継承は以下のように書けます

'use strict'

// 継承元のクラス
class SuperTest {
  constructor() {
    this.name = 'Super';
  }

  sayName() {
    return this.name;
  }
}

// 継承先のクラス
class SubTest extends SuperTest {
  sayName() {//メソッドの上書き
    return super.sayName() + 'のサブクラス';
  }

  sayHello() {//メソッドの追加
    console.log('Hello!!');
  }
}

const test1 = new SuperTest;
const test2 = new SubTest;

console.log(test1.sayName());// Super
console.log(test2.sayName());// Superのサブクラス
test2.sayHello();// Hello!!

継承元のメソッドの上書きや追加ができます。
superキーワードで継承元のクラスのメソッドにアクセスできます。PHPで言うところのparentですね。
ただし、constructorの上書きだけは注意が必要です。
これは後で説明します。

静的メソッド

クラス定義で静的メソッドも定義できます

'use strict'

//グローバルな変数
let globalCount = 0;

class Test {
  constructor() {
    globalCount++;// 呼び出すたびにカウントを増やす
  }

  static getCount() {// 静的メソッドの定義
    return globalCount;
  }
}

const test1 = new Test;
const test2 = new Test;
const test3 = new Test;

console.log(Test.getCount());// 3

こんな感じです。static宣言をすることで、クラス名.staticメソッド名で静的メソッドにアクセスできるようになります。

ゲッター・セッター

ES5で導入されたゲッターとセッターですが、クラス内でも定義できます

'use strict'

class Test {
  constructor(x, y) {
    this.x = x;// x座標を入れる
    this.y = y;// y座標を入れる
  }

  // ゲッター
  get distance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }

  // セッター
  set coodinary(arr) {
    this.x = arr.x;
    this.y = arr.y;
  }
}

const test1 = new Test(3, 4);
console.log(test1.distance);// 5
test1.coodinary = {x: 6, y: 8};
console.log(test1.distance);// 10

ゲッターはプロパティとしてアクセスされた場合ゲッターで設定された関数を実行します。セッターはプロパティに値を代入された際に、セッターで設定された関数を実行します。

クラス定義の注意点

コンストラクタの上書き時はsuperを入れるべき

クラスのコンストラクタを上書きするときだけは注意する必要があります。
まず下記のコードを見てください

'use strict'

// 継承元のクラス
class SuperTest {
  constructor() {
    this.name = 'Super';
  }
}

// 継承先のクラス
class SubTest extends SuperTest {
  constructor() {
    this.name = 'Sub'
  }
}

const test1 = new SuperTest;
const test2 = new SubTest;// この時点で落ちる

console.log(test1.name);
console.log(test2.name);

一見すると、なんでもないコードですが、SubTestのコンストラクタが動作した瞬間に「ReferenceError: this is not defined」というエラーが出て、停止します。
一方で、次のように書くと正常に動作します

'use strict'

// 継承元のクラス
class SuperTest {
  constructor() {
    this.name = 'Super';
  }
}

// 継承先のクラス
class SubTest extends SuperTest {
  constructor() {
    super();// これを追加する
    this.name = 'Sub'
  }
}

const test1 = new SuperTest;
const test2 = new SubTest;

console.log(test1.name);// Super
console.log(test2.name);// Sub

このように、継承先のコンストラクタでsuper()を入れることで、問題なく動くようになります。
コンストラクタの動きは、関数の時の動作を参考にすると、以下のようになると思います。

  1. new キーワードにより、コンストラクタが呼ばれる
  2. コンストラクタは、一旦新しいオブジェクト({})を作る
  3. 新しく作ったオブジェクトを呼び出し元としてコンストラクタの処理を実行する

しかし、コンストラクタの上書きを行うと、元のクラスのコンストラクタの機能の中で、2の機能が失われ、thisが存在しないとしてReferenceErrorが出たのだと思います。
バグかとも思いましたが、これは正式な仕様のようです

クラス定義はメソッドしか定義できない

クラス定義の中ではメソッドしか定義できません。
プロパティを定義するためにはコンストラクタで定義するか、ゲッターメソッドとセッターメソッドを作るなどで、対応は可能です
ゲッター・セッターを使う場合は以下のようになります

'use strict'

class Test {
  constructor() {

  }

  // x座標のゲッター
  get x() {
    return this._x || 0;
  }

  // y座標のゲッター
  get y() {
    return this._y || 0;
  }

  // x座標のセッター
  set x(x) {
    this._x = x
  }

  // y座標のセッター
  set y(y) {
    this._y = y;
  }

  get distance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}

const test1 = new Test;
console.log(test1.distance);// 0;
test1.x = 3;
test1.y = 4;
console.log(test1.distance);// 5

ゲッターの中で、隠しプロパティ_xが定義されていない場合は0を返すようにしてあります。
PHPよりはRubyに近いように思えます。attr_accessorはないですが。
こだわらないのであれば、prototypeを使って以下のように書いてもいいです

'use strict'

class Test {
  constructor() {

  }

  get distance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}

// 初期値設定
Test.prototype.x = 0;
Test.prototype.y = 0;

const test1 = new Test;
console.log(test1.distance);// 0;
test1.x = 3;
test1.y = 4;
console.log(test1.distance);// 5

クラスに対しプロトタイプ直接代入はできない

クラス構文は、言ってみれば関数によるクラス定義のシンタックスシュガー(糖衣構文: 読み書きしやすいように書き換えたもの)です。
実際、以下のようにクラスの方を見てみると、functionになります

'use strict'

class Test {

}

console.log(typeof Test);// function

クラス構文を使用すると、コンストラクタとメソッドを同時に定義していますが、これは関数によるクラス定義とprototypeへのメソッド登録を、別の形に書き換えているということになります。
すると、クラスのprototype自体に直接オブジェクトを代入してしまうと、クラスのメソッド定義が破壊されることになります。
実際にはそのようなことがないよう、TypeErrorによって上書きができないようになっています

'use strict'

class Test {

}

Test.prototype = {// TypeErrorが出て止まる
  someMeth: () => 'something done'
}

const test1 = new Test;
console.log(test1.someMeth());

このようなコードでは、Test.prototype =の部分で「TypeError: Cannot assign to read only property 'prototype' of class Test」というエラーを吐き出して処理が止まります

まとめ

今回はクラス定義とメソッド記法について調べてみました
Koaをやるというのと、そもそもES2015について調べてたので、ついでという意味もありました。
皆様のご参考になれば幸いです

参考

JavaScriptにもクラスがやってきた!JavaScriptの新しいclass構文をマスターしよう
Classes - JavaScript | MDN
Method definitions - JavaScript | MDN
super - JavaScript | MDN
Class構文について - JS.next