JavaScript

(初心者向け) JavaScript のクラス (ES6 対応)

More than 1 year has passed since last update.

概要

JavaScript では以前、Function を使ってクラスを実現していました。しかし、最新の JavaScript では class キーワードをサポートすることにより、他の言語と同様にクラスを実現できるようになりました。

クラスは class キーワードを使って宣言します。構文は次の通りです。

class className [extends] {
  // class body
}

クラス式を使ってクラスを宣言することもできます。構文は次の通りです。

var MyClass = class [className] [extends] {  // class body };

Function ベースのクラスについて

新しいプログラムを作るときは、新しい class キーワードを使ってクラスを作るのが、今後、普通となると思われますが、古いプログラムでは従来の Function ベースのコードが多く残っているはずです。それらを読めないと、古いコードの変更や再利用ができないので、Function ベースのクラスも理解できるようにする必要があります。

新しいクラスと古い Function ベースのクラスは混在することができます。新しいクラスの基底クラスとして Function ベースのクラスを使用することもできます。

クラス式とクラス宣言との違い

クラス式とクラス宣言はほとんど同じですが、いくつかの違いがあります。

クラス式ではクラス名 className はオプションである。一方、クラス宣言では必須。
クラス式ではクラスの再宣言が可能。一方、クラス宣言ではエラーが発生する。

class キーワードを使ってのクラス宣言の例

'use strict';

// class キーワードで Polygon を定義
class Polygon {
    constructor(height, width) {
      this.height = height;
      this.width = width;
    }

    // メソッド area()
    area() { return this.width * this.height; }
}

var p = new Polygon(3, 6);
console.log(p.width);
console.log(p.height);
console.log(p.area());

クラス式を使ってクラスを定義する例

'use strict';
// クラス式を使ってクラスを定義する。
var Polygon = class {
    constructor(height, width) {
      this.height = height;
      this.width = width;
    }

    // メソッド area()
    area() { return this.width * this.height; }    
};

var p = new Polygon(10, 5);
console.log(p.width);
console.log(p.height);
console.log(p.area());

名前付きのクラス式の例

'use strict';
// クラス式 (名前付き) を使って定義
var Polygon = class NamePolygon {
    constructor(height, width) {
      this.height = height;
      this.width = width;
    }

    // メソッド area()
    area() { return this.width * this.height; }
};

var p = new Polygon(4, 5);
console.log(p.width);
console.log(p.height);
console.log(p.area());

クラスの継承

クラスを継承するには extends キーワードを使用します。書き方は他の言語と同じです。コンストラクタは constructor という名前の特殊メソッドです。継承元の機能を利用するには、super を使用します。

次の例は、基本クラス Point を継承したクラス PointEx でメソッド distance() を追加したものです。

'use strict';

// 基本クラス Point
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

// クラス Point から派生したクラス PointEx
class PointEx extends Point {
    constructor(x, y) {
        super(x, y);
    }

    // 原点からの距離を計算する。
    distance() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }
}

var p = new PointEx(2, 4);
console.log("(%d, %d)", p.x, p.y);
console.log("distance = %d", p.distance());

実行例
(2, 4)
distance = 4.47213595499958

次の例はクラス宣言の代わりにクラス式を使った例です。

'use strict';

var Point = class {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
};

var PointEx = class extends Point {    
    constructor(x, y) {
        super(x, y);
    }

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

var p = new PointEx(2, 4);
console.log("(%d, %d)", p.x, p.y);
console.log("distance = %d", p.distance());

クラス宣言とクラス式は混在していても問題ありません。

'use strict';

// 基本クラス Point を class 宣言
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

// 派生クラス PointEx をクラス式で作成
var PointEx = class extends Point {    
    constructor(x, y) {
        super(x, y);
    }

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

var p = new PointEx(2, 4);
console.log("(%d, %d)", p.x, p.y);
console.log("distance = %d", p.distance());

組み込み型から派生することもできます。JavaScript の Date 型はそのままだと使いづらいので、好みの Date 型を作ると便利です。

'use strict';

// 組み込み型 Date から派生したクラス
class DateEx extends Date {
    // コンストラクタ y,m,d または日付指定なしを受け付ける。
    // 月は1から始めるものとする。(JavaScript の Date では1月は 0)
    constructor(...rect) {
        if (arguments.length == 3) {
            super(arguments[0], arguments[1] - 1, arguments[2]);  // 1月が0のため
        }
        else {
            super();
        }
    }

    // 文字列表現は日本式で返す。
    toString() {
        return this.toLocaleDateString('ja-jp');
    }
}

var date1 = new DateEx(2016, 1, 1);
console.log(date1.toString());
var today = new DateEx();
console.log(today.toString());

// 既存のメソッドも使用可能
console.log(today.getMonth() + 1);  // 1月が0のため

実行例
2016-1-1
2017-11-23
11

静的メソッド

静的メソッドは static キーワードを使って定義します。静的メソッドはクラスをインスタンス化せずに使えますが、その内部で this や super は使えません。

'use strict';

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    // 静的なメソッド
    static Distance(x, y) {
        return Math.sqrt(x * x + y * y);
    }
}

var p = new Point(2, 4);
var d = Point.Distance(p.x, p.y);
console.log(d);

実行例
4.47213595499958

プライベートメンバーと getter, setter

JavaScript のクラスメンバーはすべて public ですが、疑似的に private な変数を作ることができます。メソッドを変数のように使える getter, setter があるので、直接、外部からいじられたくない変数を private 変数と getter, setter を使って隠すと安全なコードを書くことができます。

プライベートメンバーを作る方法は ES2015 の Class で private なインスタンス変数 で詳しく解説されています。

次のサンプルは本来モジュールとして実装しないとメンバー変数をプライベートにすることはできませんが、あくまでテスト用と言うことで了解願います。 getter, setter は get, set キーワードをメソッドの前に付けて実現します。このようにすると、メソッドを変数のように扱うことができます。

'use strict';

// 外部非公開のシンボル
const propWidth = Symbol();
const propHeight = Symbol();

// 外部公開用のクラス
class Polygon {
    constructor(height, width) {
        this[propHeight] = height;
        this[propWidth] = width;
      }

      // メソッド area()
      area() { return this[propWidth] * this[propHeight]; }

      // プロパティ
      get Height() { return this[propHeight]; }
      set Height(value) { this[propHeight] = value; }
      get Width() { return this[propWidth]; }
      set Width(value) { this[propWidth] = value; }
}

// モジュールにするときはコメントを外す。
// export default Polygon;

// テストプログラム (動作確認後はコメントアウト)
var p = new Polygon(3, 6);
console.log(p.Width);
console.log(p.width);  // これは存在しないので undefined になる。
console.log(p.area());

p.Height = 2;
console.log(p.Height);
console.log(p.area());

実行例

6
undefined
18
2
12

メソッドのオーバーライド

メソッドのオーバーライドは特にキーワードなどなしで使えます。次のサンプルでは2重に継承を行い、メソッド distance() をオーバーライドしています。

'use strict';

// 基本クラス Point を class 宣言
class Point {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}

// 派生クラス PointEx をクラス式で作成
var PointEx = class extends Point {    
    constructor(x, y) {
        super(x, y);
    }

    // 原点からの距離を返す。
    distance() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }
};

// NaN を判別できるように改良したクラス
class PointExEx extends PointEx {
    constructor(x, y) {
        super(x, y);
        this.valid = ! (x === NaN || y === NaN); 
    }

    // オーバーライドしたメソッド
    distance() {
        if (this.valid) {
            // NaN でない場合は、基底クラスの distance() を呼び出す。
            return super.distance();
        }
        else {
            return null;           
        }
    }
}

var p = new PointExEx(2, 4);
console.log("(%d, %d)", p.x, p.y);
console.log("valid = %s", p.valid.toString());
console.log("distance = %d", p.distance());

実行例

(2, 4)
valid = true
distance = 4.47213595499958

メソッドのオーバーロード

メソッド(コンストラクタも)をオーバーロードすることはできません。代わりに、パラメータのデフォルト値を設定したり、可変数パラメータを使うなどすれば似たことが可能です。

'use strict';

var Point = class {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}

var p = new Point();
console.log("(%d, %d)", p.x, p.y);
p = new Point(5);
console.log("(%d, %d)", p.x, p.y);
p = new Point(5, -4);
console.log("(%d, %d)", p.x, p.y);

実行例

(0, 0)
(5, 0)
(5, -4)

Function ベースのクラスとの混在

class ベースのクラスも内部的には Function ベースのクラスと同じものらしいので、混在して利用可能です。したがって、古い Function ベースのクラスを継承して新しいクラスを作るなどが可能になっています。

'use strict';

// Function ベースのクラス Point
function Point(x, y) {
    this.x = x;
    this.y = y;
}

// Point を基底クラスにしたクラス PointEx
class PointEx extends Point {
    constructor(x, y) {
        super(x, y);
    }

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

var p = new PointEx(2, 4);
console.log("(%d, %d)", p.x, p.y);
console.log("distance = %d", p.distance());

実行例

(2, 4)
distance = 4.47213595499958

クラスのモジュール化と利用

クラスをモジュール化すると、メインプログラムと別ファイルなので、それぞれ別の人が作ったり管理することができて何かと便利です。

次のサンプルは、JavaScript の組み込みモジュール(日付・時刻に関するクラス) Date を使いやすくした DateEx クラスをモジュールにして利用する例です。

クラスをエクスポートする部分は下のソースで一番下の1行です。exports は Module オブジェクトのメンバーで外部に公開したい関数名やクラス名の連想配列(値がクラス名、キーは適切に名付ける)です。これにクラス名を追加することにより、外部からクラスが利用できるようになります。

※ ECMAScript6 ではエクスポート構文 export from が定義されていますが、現在 (2017-11) のところ Node.js 8.9.xLTS では実装されていないようです。

ファイル名: DataEx.js

'use strict';

// 組み込み型 Date から派生したクラス
class DateEx extends Date {
    // コンストラクタ y,m,d または日付指定なしを受け付ける。
    // 月は1から始めるものとする。(JavaScript の Date では1月は 0)
    constructor(...rect) {
        if (arguments.length == 3) {
            super(arguments[0], arguments[1] - 1, arguments[2]);  // 1月が0のため
        }
        else if (arguments.length == 6) {
            super(arguments[0], arguments[1] - 1, arguments[2], arguments[3],arguments[4],arguments[5]);  // 1月が0のため
        }
        else {
            super();
        }
    }

    // 文字列表現は日本式で返す。
    toString() {
        return this.toLocaleDateString('ja-jp');
    }

    // 現在の日付時刻を文字列で返す。
    static Now() {
        var d = new Date();
        return d.toLocaleDateString('ja-jp') + " " + d.toLocaleTimeString("ja-jp");;
    }
    // 現在の日付を文字列で返す。
    static Today() {
        var d = new Date();
        return d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
    }

    // get プロパティ
    get Year() { return this.getFullYear(); }
    get Month() { return this.getMonth() + 1; }  // 1 月は 0 でなく 1
    get Day() { return this.getDate(); }
    get Hour() { return this.getHours(); }
    get Minuite() { return this.getMinutes(); }
    get Second() { return this.getSeconds(); }
    get DayOfWeek() { return this.getDay(); }

    // set プロパティ
    set Year(value) { this.setFullYear(value); }
    set Month(value) { this.setMonth(value - 1); }  // 1 月は 0 でなく 1
    set Day(value) { this.setDate(value); }
    set Hour(value) { this.setHours(value); }
    set Minuite(value) { this.setMinutes(value); }
    set Second(value) { this.setSeconds(value); }
}


// クラス DateEx をエクスポート
exports.DateEx = DateEx;

テストプログラム testDateEx.js (DateEx.js と同じフォルダにあること)

'use strict';
const dateEx = require('./DateEx.js');

const date1 = new dateEx.DateEx(2016, 7, 3);

console.log(date1.Year);
console.log(date1.Month);
console.log(date1.Day);
console.log(date1.toString());

実行例
2016
7
3
2016-7-3

Function ベースのクラスは何が問題なのか

なぜ、Function ベースのクラスが問題なんでしょうか?
機能的には class キーワードを使ったクラスと同じです。

問題点は「読みにくさ」にあると思います。他の言語と同じように class が使えることにより、他の言語の経験者はクラスの機能が容易に理解できますし、習得が速くなります。

他の言語の経験者はこれがクラスには見えませんね。

/* クラスを関数として定義する。*/
function Point(x, y) {
  this.x = x;
  this.y = y;

  // メソッド distance()
  this.distance = () => {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}

var p = new Point(10, 5);
console.log(p.x);
console.log(p.y);
console.log(p.distance());

これはクラスの継承ですが、もっとわかりづらいです。prototype って何?、これを知らないと意味が分かりません。

/* メソッド distance の追加 */
function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.distance = function() {
   return Math.sqrt(this.x * this.x + this.y + this.y);
}

var p = new Point(4, 3);
console.log(p.distance());

終わり