🌌

[JavaScript] クラス構文による独自データ型とパターンマッチ

2021/08/29に公開

[JavaScript] クラス構文による独自データ型とパターンマッチ

生jsで素朴にオブジェクト指向プログラミングをするまでの理解の手順
jsのクラス構文による独自データ型とパターンマッチできるまでがゴール

目次

jsでの"クラス"

jsにおいて全てクラスめいたものはfunctionオブジェクトとして定義されている

typeof Object // -> 'function'
typeof class{} // -> 'function'

jsでのfunctionオブジェクト

データと機能の集合≒オブジェクト
第一級として振る舞うデータと処理のまとまり

プロトタイプベースのオブジェクト指向

__proto__がネストしていくことで継承を実現している(プロトタイプチェーン)

Screenshot - ebab7d6b799e74fccdab6016198af7ca - Gyazo

プロトタイプチェーン

  1. インスタンス作成時にインスタンスの__proto__プロパティにプロトタイプオブジェクトの参照を保存
  2. インスタンスからメソッドを参照するとき__proto__内部プロパティまで辿る

jsでのclass構文

objectとprototypeを簡単に書くための糖衣構文
実体はfunctionオブジェクト

typeof class {} // -> 'function'

class宣言とclass式、名前付きclass式の3種類
特に明示的にプロトタイプチェーンを設定しなくて済む

functionベース

function Hoge(){}

function Fuga(){}

Fuga.prototype.__proto__ = Hoge.prototype;

classベース

class Hoge {}

class Fuga extends Hoge {}

Fuga.prototype.__proto__ === Hoge.prototype
// -> true

なぜObject()はできてclass Hoge {} のHoge()はできないのか

そもそもビルトインオブジェクトのObjectはfunctionベースで定義
class構文ではfunctionとして呼び出されないようにHoge()でのコンストラクタ呼び出しを禁止している(ランタイムエラーになる)

class Hoge() {}
Hoge()
// Uncaught TypeError: Class constructor Hoge cannot be invoked without 'new' at <anonymous>:1:1

ビルトインオブジェクトの大半はfunctionベースでクラスめいたものとして定義されてる
が、比較的新しいMapオブジェクトとかはMap()はできない

プロトタイプメソッドとインスタンスメソッドの違い

  • 定義の仕方

    • インスタンスメソッド
      • thisに気をつけてconstructorにアロー関数で定義
    • プロトタイプメソッド
      • class宣言/式直下にメソッドとして定義 -> prototypeにメソッドが生える
  • 継承のされ方

    • インスタンスメソッド
      • constructorのsuperでインスタンスに動的にinject
    • プロトタイプメソッド
      • 親のprototypeにある定義を辿る

インスタンスメソッド > プロトタイプメソッド

同時に定義できるがオーバーライドではない
そのため、インスタンスメソッドをdeleteするとプロトタイプメソッドが参照されるようになる

class Hoge {
  constructor(){
    this.greet = () => console.log('hello') // インスタンスメソッド
  }
  greet() { // プロトタイプメソッド
    console.log('こんにちは')
  }
}

const hoge = new Hoge()
hoge.greet() // -> 'hello'
delete hoge.greet
hoge.greet() // -> 'こんにちは'

プロトタイプチェーンのフォールバック

Screenshot - ad3a8b65483fafffd1325f595c21fe37 - Gyazo
dynamic languages - How does JavaScript .prototype work? - Stack Overflow

インスタンスが所属するクラスとそのクラス名の取得

class Class {}
const class = new Class()
class.constructor === Class // -> true
class.constructor.name // -> "Class"

jsのクラス構文による独自データ型とパターンマッチ

jsで素朴なオブジェクト指向プログラミング

class Animal {
    constructor(num) {
        if(num === undefined) throw 'animal type number must exist';
        this.num = Number(num)
    }

    get bark(){ throw 'bark must be implemented' }
    get fight(){ throw 'fight must be implemented' }

    get act(){
        this.bark
        this.fight
    }

    static doSomething(animal) { // like pattern matching
      if(animal.constructor === Dog) return console.log('do some dog execution');
      if(animal.constructor === Cat) return console.log('do some cat execution');
      //if(animal.constructor.name === Dog.name) return console.log('do some dog execution');
      //if(animal.constructor.name === Cat.name) return console.log('do some cat execution');
      throw 'not match any animal type'
    }

    static parse(str) {
        if(str === "Dog" || str === 0 || str === "0") return new Dog();
        if(str === "Cat" || str === 1 || str === "1") return new Cat();
        throw 'cannot parse to any animal type'
    }
}

class Dog extends Animal {
    constructor(){
        super(0)
    }
    get bark(){ return console.log("bowwow") }
    get fight(){ return console.log('bite') }
}
class Cat extends Animal {
    constructor(){
        super(1)
    }
    get bark(){ return console.log("meow") }
    get fight(){ return console.log('run away') }
}

const dog = new Dog()
const cat = new Cat()
const fish = new (class Fish{})()

dog.num // -> 0
cat.num // -> 1
//fish.num // -> undefined

Animal.doSomething(dog) // -> do some dog execution
Animal.doSomething(cat) // -> do some cat execution
// Animal.doSomething(fish) // -> Uncaught animal type number not found

Animal.parse("Dog") // -> Dog {num: 0}
Animal.parse(1) // -> Cat {num: 1}
// Animal.parse(2) // -> Uncaught cannot parse to any animal type

dog.act
// -> bowwow
// -> bite
cat.act
// -> meow
// -> run away

上記から特にパターンマッチ部分を抽出

static doSomething(animal) { // like pattern matching
  if(animal.constructor === Dog) return console.log('do some dog execution');
  if(animal.constructor === Cat) return console.log('do some cat execution');
  //if(animal.constructor.name === Dog.name) return console.log('do some dog execution');
  //if(animal.constructor.name === Cat.name) return console.log('do some cat execution');
  throw 'not match any animal type'
}

クラスの一致/不一致でパターンマッチめいたものを実装する
コンパイラの恩恵は受けれないため実装漏れはランタイムエラーを投げる

Discussion