LoginSignup
23

More than 5 years have passed since last update.

JavaScriptのコンストラクタとかいう闇深いものとprivate

Last updated at Posted at 2019-02-18

JavaScriptのコンストラクタとかいう闇深いものとprivate

js初心者が勉強中にコンストラクタの変な挙動を見つけて気になったので少し実験をしてみた。

目次

constructorとは

オブジェクトの生成と初期化のための特殊なメソッドです。

要はnewしたときに自動で実行される特別なメソッドです

定義の仕方多すぎる問題

動くパターンと動きそうで動かないパターンがあるのでまずは動くパターン

動くパターン

関数

動くパターン
function A() {
    console.log('class A')
}

console.log(new A)
// class A
// A {}

これはA自体がコンストラクタになっているパターン

クロージャ

動くパターン
var B = function() {
    console.log('class B')
}

console.log(new B)
// class B
// B {}

これもAと同じです

クラス

動くパターン
class D {
    constructor() {
        console.log('class D')
    }
}

console.log(new D)
// class D
// D{}

クラスDにconstructorメソッドを定義するパターン
しかし以下の方法はコンストラクタとして正しく定義できません

動かないパターン

アロー関数

動かないパターン
var C = () => {
    console.log('class C')
}

console.log(new C)
// Uncaught TypeError: C is not a constructor

アロー関数式 は、その名のとおり矢印を使って記述し、function 式より短い構文で同様な内容を記述することができます。なおthis, arguments, super, new.target を束縛しません。また、アロー関数式は、メソッドでない関数に最適で、コンストラクタとして使うことはできません。
アロー関数

関数にconstructorメソッドを定義

動かないパターン
function E() {
    this.constructor = function() {
        console.log('class E')
    }
}

console.log(new E)
// E {constructor: ƒ}

Eのconstructorメソッドは実行されません(なんで)

関数のprototypeにconstructorメソッドを定義

動かないパターン
function F() {
}
F.prototype.constructor = function() {
    console.log('class F')
}

console.log(new F)
// F {}
// __proto__:
// constructor: ƒ ()

この場合もFのconstructorメソッドは実行されません(なんで)

自身のクラスのインスタンス以外を戻り値にできる問題

new演算子について調べるとこんな記述が

コンストラクタ関数が返すオブジェクトが、new 式の結果になります。コンストラクタ関数が明示的にオブジェクトを返さない場合は、ステップ 1 で生成したオブジェクトを代わりに使用します。(通常、コンストラクタは値を返しませんが、通常のオブジェクト生成プロセスをオーバーライドしたい場合はそのようにすることができます)
new

どうやらオブジェクトを返すと通常とは違う動きをするらしい

コンストラクタに戻り値を持たせてみた

戻り値がオブジェクト以外
class G {
    constructor() {
        return 1
    }
}

console.log(new G)
// G {}

オブジェクト以外の場合は戻り値が無視されています

戻り値がオブジェクト
class H {
    constructor() {
        return {hoge:'hoge'}
    }
}

console.log(new H)
// {hoge:'hoge'}

戻り値が返って来る
Hはどこかへ行ってしまった。。。
※配列の場合も同じ挙動になります

メソッドやプロパティはどうなるのか

class I {
    constructor() {
        this.hoge = 'hoge'
        return {}
    }

    fuga() {
        return 'fuga'
    }
}

var i = new I
console.log(i)
// {}
console.log(i.hoge)
// undefined
console.log(i.fuga())
// Uncaught TypeError: i.fuga is not a function

まったく別のオブジェクトが返ってくるのでプロパティやメソッドにアクセスできない
だがプロパティやメソッドを戻り値のオブジェクトに持たせることはできる様子

戻り値のオブジェクトにプロパティやメソッドを持たせる
class J {
    constructor() {
        this.hoge = 'hoge'
        return {
            hoge : this.hoge,
            fuga : this.fuga,
        }
    }

    fuga() {
        return this.hoge
    }
}

var j = new J
console.log(j)
// {hoge: "hoge", fuga: ƒ}
console.log(j.hoge)
// hoge
console.log(j.fuga())
// hoge

上記の例の戻り値のプロパティとメソッドは戻り値のオブジェクト自身が持っていることになっている
つまり戻り値のhogeプロパティはJクラスから分離されている
fugaメソッド内でつかうthisは戻り値のオブジェクトを参照する

継承するとconstructorが壊れる問題

以下の、コンストラクタでオブジェクトを返す親クラスがあったとする

親クラス
class Parent {
    constructor() {
        return {}
    }
}

このParentクラスを継承して新しいクラスをつくる
そしてコンストラクタ内でプロパティを定義する

子クラス
class Child extends Parent {
    constructor() {
        super()
        console.log(this)
        this.hoge = 'hoge'
    }
}

console.log(new Child)
// {}
// {hoge:'hoge'}

コンストラクタ内の三行に注目していただきたい

1行目
この行は親クラスのコンストラクタを実行している
これがないとエラーになる

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

thisを使いたければスーパークラスのコンストラクタを実行しろ的な

2行目
1行目のおかげでthisが使えるようになったので確認してみると中身は親のコンストラクタが返した空のオブジェクト。
つまり一行目ではvar this = super()のようなことがされている感じ

3行目
2行目から分るとおりthisは親のコンストラクタが返した空のオブジェクトなのでChildクラスにプロパティを定義することはできない
Childクラスに他のメソッドを定義してあっても呼び出すことはできない

このようにコンストラクタでオブジェクトを返すと継承先のコンストラクタを汚染することになる

これを使ってなにができるか

privateを実装することができるぽい?

基本概念

Bridgeパターンのような(Bridgeパターンをよく分かってない。。。)橋渡しをしてくれるオブジェクトを、コンストラクタで返す
ただしそのままオブジェクトにプロパティやメソッドを定義していってもthisを指すものがコンストラクタで返すオブジェクト自身になってしまうため工夫をする

メソッド

bind()を使ってthisが何を指すのかを指定する

...
    constructor() {
        this.name = 'hoge'
        return {
            getName : this.getName.bind(this)
        }
    }

    getName() {
        return this.name
    }
...

これでgetName内で、コンストラクタの戻り値で返していないthis.nameを指せるようになった

これがコンストラクタでprivateを実装する基本的な方法です
ようはpublicにしたいものを定義していく方法です

しかしこのままではasyncメソッドは定義できない
なので改造

...
    constructor() {
        this.name = 'hoge'
        return {
            getName : function() {
                return this.getName()
            }.bind(this)
        }
    }

    async getName() {
        return this.name
    }
...

これでasyncメソッドでも実行結果を返すので問題ない(はず)

プロパティ

安直に以下のようにすると正しく動かない

...
    constructor() {
        this.name = 'hoge'
        return {
            name : this.name
        }
    }
...

this.nameを格納しているので、外から値を変更できない
なので改造

...
    constructor() {
        this.name = 'hoge'
        let bridge = {}
        Object.defineProperty(bridge, 'name ', {
            get: function() {
                return this.name
            }.bind(this),
            set: function(arg) {
                this.name = arg
            }.bind(this)
        })
        return bridge
    }
...

Object.definePropertyを使ってコンストラクタで返すオブジェクトにgetter setterメソッドを定義してbind()する

これで外でプロパティを変更しても反映される

継承してコンストラクタを使いたいときの救済措置

この方法でprivateを表現するとコンストラクタが壊れてしまう
これはどうしようもないです
なので救済措置

コンストラクタ内のthisが汚染されるだけなので、_init_()メソッド(名前はなんでもいいです)を別に用意してこれをコンストラクタ代わりにする

可変長引数で引数の数は子クラスに自動であわせられる

親クラス
...
    constructor(...initArgs) {
        this._init_(...initArgs)
        return {
            ...
        }
    }

    _init_() {}
...
子クラス
...
    _init_(argA, argB) {
        super._init_()
        this.hoge = argA
        this.fuga = argB
    }
...

でもpublicで返したいものは全部親クラスがなんとかしないといけない
継承したら子クラスのプロパティやメソッドpublicにできないよね...
親クラスが子クラスの実装内容知ってたら気持ち悪いし
ていうかいちいちpublicなやつ定義するのめんどくさいよね...

だから全部自動でpublicなやつ定義する

最終結果

簡単にprivateできるクラスが作れます

使い方

  • 下記のクラスを継承する
  • 一文字目が_、二文字目が_以外のプロパティとメソッドはprivateになる
    • this.hogeはパブリック
    • this._hogeはプライベート
  • 初期化処理は_init_()を使う

コード

class Base {
    constructor(...initArgs) {
        // 継承先用の独自コンストラクタ
        this._init_(...initArgs)

        // コンストラクタで返すオブジェクト
        let bridge = {}

        // プロパティとメソッド全部取得
        let obj = this
        let propNames = []
        while (obj) {
            propNames = propNames.concat(Object.getOwnPropertyNames(obj))
            obj = Object.getPrototypeOf(obj) // __proto__も全部見る
        }

        // privateのパターン
        let privatePattern = /^_[^_].*/
        // publicにするやつ全部定義する
        for (let propName of new Set(propNames)) {
            if (privatePattern.test(propName) || propName === '__proto__') {
                // privateのパターンにマッチするか__prop__のときはcontinue
                continue
            }

            if (typeof this[propName] === 'function') {
                // 関数の場合
                bridge[propName] = function(...args) {
                    return this[propName](...args)
                }.bind(this)
            } else {
                // プロパティ(getterも含む)
                Object.defineProperty(bridge, propName, {
                    get: function() {
                        return this[propName]
                    }.bind(this),
                    set: function(arg) {
                        this[propName] = arg
                    }.bind(this)
                })
            }
        }
        return bridge
    }

    _init_() {}
}

問題点

  • このクラスを継承しなければならない
  • コンストラクタが汚染されるので代わりの_init_()を使わなければならない
  • privateなstaticメソッドは定義できない
  • インスタンスした結果をconsole.logしてもどのクラスなのか分りにくい
    • メソッドの[[BoundThis]]を見れば一応分る
  • インスタンスするたびにオブジェクトつくるのでたくさんインスタンスするのは重そう
    • bridgeをstaticプロパティにしてうまくディープコピーできればよさげ(やり方教えてください)
  • IEは知らないです

最後に

js難しいです。
間違っていることや足りないことがあれば教えていただけると嬉しいです
__init__()はPythonのコンストラクタの名前をもらいましたが隠蔽したほうがよさそうなので_init_()とかのほうがいいかもです
_init_()にしました

全然privateじゃありませんでしたこれprotectedでした^^

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23