JavaScript・TypeScriptのclassの取り扱いについて 糖衣構文とプロトタイプチェーン

2020年02月24日 00時02分

2015 年(ES6)から class 記法で実装できるようになりました。

prototype 記法から逃れてずいぶん経ちます。

TypeScript も当たり前ですがトレンドを追っているので class 記法で表記することができます。ただ、オブジェクト指向のクラスと厳密には異なります。

class の取り扱いと prototype チェーンをまとめておきたいと思います。

プロトタイプモデルの糖衣構文として class

今回は TypeScript で class を表記しています。

クラス記法、フィールド、コンストラクタ、そしてメソッドの設定です。

ここだけを見るとオブジェクト指向と似たような構文に見えます。

実際にコンパイルしてみるとどうなるでしょうか。

//コンパイル前 ts file
class Dog {
    private name:string
    constructor(name:string){
        this.name = name
    }
    howl(){
        console.log(`${this.name} howling at some people`)
    }
}

コンパイル後のソースがこちらになります。

//コンパイル後
var Dog = /** @class */ (function () {
    function Dog(name) {
        this.name = name;
    }
    Dog.prototype.howl = function () {
        console.log(this.name + " howling at some people");
    };
    return Dog;
}());

Dog は関数を持つメソッドとして表記されています。

あくまで class 記法は糖衣構文(読み書きのしやすさのために導入される書き方)であり、JavaScript のプロトタイプモデルを理解していく必要があるのです。

ECMAScript 2015 で導入された JavaScript クラスは、JavaScript にすでにあるプロトタイプベース継承の糖衣構文です。クラス構文は、新しいオブジェクト指向継承モデルを JavaScript に導入しているわけではありません

JavaScript リファレンス

クラス

プロトタイプチェーン

JavaScript の class 記法はプロトタイプベースの糖衣構文であるということが分かりました。では、class と prototype で異なる点はどこでしょうか。

下記は Dog を継承して Dachshund を設定しました。

Dachshund クラスは空の実装ですが、インスタンス生成時に name を受け取っています。また、howl も Dog の実装から継承されて動作しています。

この点をみるとオブジェクト指向にある継承は問題なく出来ていそうです。

//ts file
class Dog {
    private name:string
    constructor(name:string){
        this.name = name
    }
    howl(){
        console.log(`${this.name} howling at some people`)
    }
}

class Dachshund extends Dog{
}

let pochi = new Dachshund("POCHI")
pochi.howl();//POCHI howling at some people

次にこの継承元のオブジェクトのメソッドを変更したものを追記します。

Dog.prototype.howl 以下が追記部分です。

スーパークラスのメソッドが変わっても、インスタンスのメソッドは変わらないと思いますが残念ながら変化していまいます。

これにはプロトタイプモデルの特徴であるプロトタイプチェーンが関わってきています。

// ts file
class Dog {
    private name:string
    constructor(name:string){
        this.name = name
    }
    howl(){
        console.log(`${this.name} howling at some people`)
    }
}

class Dachshund extends Dog{
}

let pochi = new Dachshund("POCHI")
pochi.howl();//POCHI howling at some people

Dog.prototype.howl = function(){
    console.log(`garr`)
}
pochi.howl();//garr

プロトタイプチェーンは、スーパークラスが存在しており、かつそのクラスに存在しないプロパティがある場合、スーパークラス側に参照が行くという動作を繰り返してチェーンのようになっている動きを指します。

今回でいえば、インスタンスに howl()が存在していないため、スーパークラスである Dog 内が参照され、howl()を呼んでいます。

Dog 参照されるという点がミソで、スーパークラスのメソッドを上書きしてしまうと、以降の呼び出しでは上書き後のメソッドが実行されてしまいます。

これが、pochi.howl();で garr がログに出力されてしまう理由です。

まとめ JavaScript の class 記法はオブジェクト指向のものとだいぶ違った動きをするよね

まとまりました。

JavaScript の class 記法は糖衣構文なので、オブジェクト指向の動きを求めるのはちょっと違うなぁという感じです。

とはいえ web を触っている以上は避けることができないので適応していきましょう。継承の概念は実装の重複をなくしたり関数をラップして概念として取り扱うことができるので使いこなしていきたい。

bingo.web 2でもお待ちしています。