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でした^^