[Work/Class/ES2015で動的Webアプリ/ES2015Basic]

ES2015の基本 - クラスの定義とインスタンス化,プロパティ / 2017-07-27 (木)

クラスの定義

基本的なクラスの定義

以下のように

class BasicBall{
    // コンストラクタの定義
    constructor(windowWidth, windowHeight){
        this.windowWidth;
	this.windowHeight;

	this.xPos = Math.floor(Math.random() * this.windowWidth);
	this.yPos = Math.floor(Math.random() * this.windowHeight);
    }

    // インスタンス関数の定義
    updateParameters(){
        this.xPos += 10;
	this.yPos += 10;
    }

    // 同じくインスタンス関数
    getXPos(){
        return this.xPos;
    }

    getYPos(){
        return this.yPos;
    }
}

クラスを定義し,

let myBall = new BasicBall(640, 480);

でインスタンス化する.

コンストラクタがconstructorという名前の関数であること以外は,基本的にはJavaと同じであるが,注意点は,クラスのインスタンスメンバ変数宣言が存在しないことである(つまりPythonと同じく関数宣言しかない).コンストラクタ内部でthis.変数名 = 1;のように必ず「このクラスのメンバ変数である」という感じで宣言する必要がある.

これは後述の「プロパティ」という仕様による.

ゲッターとセッター

コンストラクタやインスタンスメンバ関数内で宣言されたthis付きの変数は全てprivate扱いとし,直接アクセスは避けるようにする.(明示的にprivateにするためには様々な方法があるが「private扱いである」ことにしておくとよい)

ただし,

get x(){
    return this._xPos;
}

get y(){
    return this._yPos;
}

set x(xPos){
    this._xPos = xPos;
}

set y(yPos){
    this._yPos = yPos;
}

というようにgetsetというキーワードをつけて,あらかじめコンスタクタで定義した変数(の頭にアンダースコア「_」をつけたもの)の中身を取り出したりいじったりする関数(ゲッターgetterとセッターsetterと呼ぶ)を定義しておくと,

let myBall = new BasicBall(640, 480);
let drawingX = myBall.x;
let drawingY = myBall.y;
myBall.x = Math.floor(Math.random() * 640);
myBall.y = Math.floor(Math.random() * 480);

という感じで,あたかもメンバ変数がpublicであるかのように,そのままアクセスできるようになるのだが,getなんちゃら()関数,setなんちゃら()関数のように専用関数を作る方が,カプセル化として明確である.(読めるプログラマが多い)

クラスの継承

先ほどのBasicBallは,右下に単調移動するだけのクラスだったので,これを継承して跳ね返るボールのクラスを定義する.

class Bounceball extends BasicBall {
    // コンストラクタ
    constructor(windowWidth, windowHeight){
        super(windowWidth, windowHeight); 親クラスのコンストラクタを最初に呼び出す
	if(Math.random() > 0.5){
	    this.xDirection = 1;
	}
	else{
	    this.xDirection = 1;
	}
	
	if(Math.random() > 0.5){
	    this.yDirection = 1;
	}
	else{
	    this.yDireciton = -1;
	}
    }

    // 関数のオーバーライド
    updateParameters(){
        this.xPos += this.xDirection * 10;
	this.yPos += this.yDirection * 10;

	if(this.xPos > this.windowWidth){
	    this.xDirection = -1;
	}
	else if(xPos < 0){
	    this.xDirection = 1;
	}

	if(yPos > this.windowHeight){
	    this.yDirection = -1;
	}
	else if(yPos < 0){
	    this.yDirection = 1;
	}
    }
}

基本的にJavaと一緒である.「@Override」がないので,コメントでオーバーライドであることを明示すると良い.

プロパティ

プロパティの概要

JavaScriptでは「関数オブジェクト」が存在すると前項で書いたが,関数自体も変数に打ち込めることからわかる通り,変数と関数の区別がない.従ってクラスをnewでインスタンス化したものの中では,変数と関数は「プロパティ」という括りで同一に扱うことができる.

インスタンスを作った後にプロパティを追加する

Javaでは,クラスとそのインスタンスに機能(変数や関数)を追加する場合,あらかじめクラスを継承した子クラスを作っておき,その子クラスのインスタンスを生成する,と言う明快な方法をとる.

ところがJavaScriptでは「メンバ変数,メンバ関数」ではなく,「プロパティ」であるため,インスタンス化をした後にメンバ変数とメンバ関数を追加することができてしまう.

上記のクラスBounceBallのインスタンスを作った後に,「そのインスタンスに対して」色要素を追加したい場合,

let myBounceBall = new BounceBall(640, 480);
myBounceBall.rColor = Math.floor(Math.random() * 155) + 100;
myBounceBall.gColor = Math.floor(Math.random() * 155) + 100;
myBounceBall.bColor = Math.floor(Math.random() * 155) + 100;

という風に書くと,メンバ変数のようにプロパティを追加できる.

さらに,

myBounceBall.getRed = function(){
    return this.rColor || 0; //this.rColorがない時は0を返す,という意味
};

と言う風に,関数ですらプロパティとして追加できる.

window.onloadとwindow.onunloadの正体

つまりwindow.onload = function(){...};window.onunload = function(){...};という指定は,このプロパティである関数オブジェクト(を保持する変数の中身)を,新たに作った関数オブジェクトで上書きしている,ということになる.(よってクラスの継承によるオーバーライドとは原理が違う)

window.addEventListenerによるwindow.onloadで実行する関数を追加

window.addEventListener('イベントのタイプ名', '関数名'もしくは関数オブジェクト, false);

のように,addEventListener関数をwindowオブジェクトは持っているのだが,この「イベントのタイプ名」に'load'文字列を指定してやると,onload関数を,新しい関数オブジェクトで「上書き」するのではなく,新たに関数オブジェクトを「追加」することができる.

つまりonloadはプロパティの名前であり,'load'がイベントタイプである.イベントタイプには例えば「'click'文字列」などを指定することができる.(JavaScriptのボタン動作に'onclick'文字列を指定していたことを思い出すこと)

let myOnloadFunction = {alert('onloadの上書き関数');};
window.addEventListener('load', myOnloadFunction, false);

ただし,addEventListenerはInternetExplorerでは使えないので,attachEventで指定する.(こちらのイベントタイプには「on」がつく)

window.attachEvent('onload', myOnloadFunction);

と指定する.

IEと他のブラウザを両立させたい場合,

if(window.addEventListener){
    // もしwindowがaddEventListenerプロパティ関数を持っている場合,
    window.addEventListener('load', myOnloadFunction, false);
    //myOnloadFunction関数オブジェクトをonload処理に追加する
}
else if(window.attachEvent){
    // IEだと上のif文はfalseになり,こちらがtrueになる
    window.attachEvent('onload', myOnloadFunction);
}
else{
    // 両方持ってない場合,しょうがないので上書きする.
    window.onload = myOnloadFunction;
}

のように書くことができる.

deleteキーワードによるプロパティの削除

delete インスタンス.プロパティ;で,プロパティを削除することができる.

グローバルな変数や関数は,windowインスタンス(Node.jsではglobalインスタンス)がプロパティとして持っていることを思い出すと,delete window.グローバル変数名;でグローバル変数(や関数)を削除することができる.

正確には仕様としてdeleteでメモリ解放が定められているわけではなく,あくまで「プロパティの削除」なのだが,大体のブラウザは「どの変数も保持しなくなったオブジェクトは消す」というガベージコレクションが働くようになっている.

SafariはChromeよりも使えるメモリ量が少ないので,Safariでメモリが足りない場合など.

コールバック関数中におけるthisキーワードとアロー関数定義

前項で出てきた,関数オブジェクトを生成する3つの手法のうち,マジでわけがわからない記法である「let 関数オブジェクトを保持する変数 = (引数) => {関数の中身};」というアロー関数記述であるが,実は非常に重要な用法がある.

クラスのメンバ関数の定義は上記のような通常のJavaやPythonに似たクラス指定で問題ないのだが,問題は自分で設計・記述したクラスのメンバ関数中から呼ばれるコールバック関数である.

JavaScriptの標準のライブラリでは,クラスになっていない関数単体であったり,クラスになっていてもES2015的なクラス継承でのオーバーライド記述に対応できていないものが多いようで,コールバック関数の指定などは従来のようにプロパティに関数オブジェクトを入れる(上書きする)形でのみ動くものが多いようである.

コールバック関数の呼び出しは,クラスのインスタンスが行うのではなく,グローバル(window,Node.jsならglobal)のインスタンスが呼び出している.グローバルインスタンスが呼び出した場合,例えクラスのメンバ関数として宣言されている関数をコールバック関数に登録したとしても,関数定義の中で使われているthisキーワードはグローバルインスタンス自身を指すことになる.

例えば以下の例を考えてみる.

class MyClass{
    constructor(){
        this.myVariable = 'メンバ変数';
    }

    startTimer(){
        setTimeout(function(){
	    alert(this.myVariable);
	}, 1000);
    }
}

window.addEventListener('load', function(){
    let instanceOfMyClass = new MyClass();
    instanceOfMyClass.startTimer();
});

このコードを実行する(実行例)と「myVariableはundefinedになってしまう」,つまり「定義されていない」エラーが出てしまう.これはsetTimeout関数にセットしたコールバック用の関数オブジェクトを呼び出すのはグローバルのインスタンスだからであり,グローバルインスタンスはmyVariableメンバ変数やメンバ関数(二つをまとめて「プロパティ」)は持っていないからである.

そこでコールバック関数のための関数オブジェクトを生成する時にアロー記法を使うと,「その関数オブジェクトが記述された場所のスコープがthisの中に入る」.

class MyClass{
    constructor(){
        this.myVariable = 'メンバ変数';
    }

    startTimer(){
        setTimeout(() => {
	    alert(this.myVariable);
	}, 1000);
    }
}

window.addEventListener('load', function(){
    let instanceOfMyClass = new MyClass(); 
    // これの変数も「function」キーワードで関数オブジェクトを作っているので,
    // 実行時はグローバルになっていることに注意
    instanceOfMyClass.startTimer();
});

とアロー関数記述を使う(実行例)と,コールバック関数が呼ばれた時のthisにはinstanceOfMyClassというインスタンスが入るので,undefinedエラーは起きずに,ちゃんと「メンバ変数」という文字列が表示される.

さらに,以下のようにaddEventListener内で宣言した変数をグローバルインスタンスwindowのプロパティとするように明示して書いておくと,より明確になる.

... // MyClassの定義は上記の通りなので省略
window.addEveventListener('load', function(){
    window.instanceOfMyClass = new MyClass();
    window.isntanceOfMyClass.startTimer();
});