JSのコンストラクタは別のクラスのオブジェクトも返せる

お前は何を言ってるんだ?という感じの記事タイトルだが、JavaScriptのnew演算子はそのクラスと全く関係ないオブジェクトを返すことができるらしい

developer.mozilla.org

If the constructor function returns a non-primitive, this return value becomes the result of the whole new expression. Otherwise, if the constructor function doesn't return anything or returns a primitive, newInstance is returned instead. (Normally constructors don't return a value, but they can choose to do so to override the normal object creation process.)

テキトーに翻訳、

コンストラクタが非プリミティブ値を返した場合、この戻り値がnew式全体の結果となる。もしコンストラクタ関数が値を返さなかったりプリミティブを返した場合は、代わりに newInstance が返される。(普通はコンストラクターは値を返さないが、値を返すことで通常のオブジェクト生成プロセスをオーバーライドすることもできる)

しれっととんでもないことが書いてある。要するに「コンストラクタは暗黙的にthisを返すけど、非プリミティブな値を明示的に返すこともできまっせ」ってことか。えー、つまりこういうこと?

class Dog {}

class Cat {
  constructor() {
    return new Dog()
  }
}

console.log(new Cat())  // -> Dog { } と出力

new CatでDogが返されるというクソコードが爆誕した🙀🙀🙀🙀

「通常のオブジェクト生成プロセスをオーバーライド」ってどういうこと?

コンストラクタに自身のインスタンスが渡されたらスルーしてそのまま返すとか・・・

class Cat {
  constructor(arg) {
    // Catオブジェクトが渡されたらそのまま返す
    if (arg instanceof Cat) {
      return arg
    }
    // それ以外なら通常のオブジェクト生成を実行
    this.name = arg
  }
}

const cat1 = new Cat("ニャン")
const cat2 = new Cat(cat1)

console.log(cat1)            // Cat { name: 'ニャン' }
console.log(cat2)            // Cat { name: 'ニャン' }
console.log(cat1 === cat2);  // true

シングルトンを返すとか・・・

class Cat {
  static singleton = new Cat();

  constructor() {
    return Cat.singleton;
  }
}

const cat1 = new Cat()
const cat2 = new Cat()
const cat3 = new Cat()

console.log(cat1);           // Cat { } 
console.log(cat1 === cat2);  // true
console.log(cat1 === cat3);  // true

これ、なぜsingletonを初期化できるのか不思議なコードなのですが、コンストラクタの仕様によるトリックで意図通り動作します。

static cat = new Cat(); の部分で初めてコンストラクタが呼ばれたとき、static変数singletonはまだ初期化されていないのでundefindがreturnされますが、「コンストラクタがプリミティブをreturnした場合、その戻り値を無視して新しく生成したオブジェクトを返す」という仕様のためCatオブジェクトが返されてsingletonが初期化されます。そして2回目以降のnewでは初期化済みのsingletonが返されるというトリックになっています

Golangのインターフェースは拡張していいものなのか?

早速Golangで頭を抱えている

Goには「このインターフェースを実装するぞ!」という明示的な宣言がない。errorインターフェースがいい例だが、Error() stringというシグニチャのメソッドがある構造体は何でもerrorとして扱うことできるようになる。ダックタイピングのような挙動を示す

そんなインターフェースの型によって分岐する単純なコードを考える

type Animal interface {
    Bark() string
}

// animalを実装したCat
type Cat struct {}

func (cat *Cat) Bark() string {
    return "Nyan"
}

// animalを実装してないApple
type Apple struct{}

func whatIs(x any) {
    switch x.(type) {
    case Animal:
        fmt.Println("Animal")
    default:
        fmt.Println("Not Animal")
    }
}

これを実行すると

func main() {
    whatIs(&Cat{}      // => Animalと出力
    whatIs(&Apple{})    // => Not Animalと出力
}

ここで迂闊にAnimalインターフェースを修正してしまった

type Animal interface {
    Bark() string
    Run()
}

そしてCatの修正を忘れてを実行すると

func main() {
    whatIs(&Cat{}      // => Not Animalと出力
    whatIs(&Apple{})    // => Not Animalと出力
}

Catを直していないのでNot Animalになってしまった・・・

明示的なインターフェース実装宣言がないので警告も出ずコンパイルも通ってしまうが意図せぬバグとなる可能性がある

安全なコードを書くという観点でどう扱えばいいのか悩ましい

そもそもインターフェース型でディスパッチすべきではない???

型で分岐しなければ問題ない。まあ、そうなんだけど納得できる答えではないなあ

モノをインターフェースにしない???

これはJavaのインターフェースの考え方だが、モノではなく能力をインターフェースにする。今回の場合はAnimalではなくBarkableのような名前のインターフェースを用意し、

type Barkable interface {
    Bark() string
}

ここにRunというメソッドを足そうというやつはいないはずだ・・・

テストかけ

ど正論、でもコンパイル型言語はそういうとこのミスはコンパイラに見つけて欲しいよな

結論

わからん