JavaScriptでprivate「WeakMap」編
コンストラクタの闇とそれを使った private (本当はprotectedだった)について書いたら、Symbolでprivate実現できると教えていただきました。
調べてみたらいくつかの方法でそれっぽいことができるようなので勉強がてら整理してみた(何番煎じだよ!!)
今回はWeakMap編です
コンストラクタの闇↓
JavaScriptのコンストラクタとかいう闇深いものとprivate
Symbol編↓
JavaScriptでprivate「Symbol」編
WeakMapとは
WeakMap オブジェクトは、キーはオブジェクトのみで、値は任意の値にできるキー / バリューのペアからなるコレクションです。
Map オブジェクトとの違いの1つは、WeakMap のキーは列挙可能ではないことです(すなわち、キーのリストを取得するメソッドがありません)。もしも列挙可能であれば、リストは非決定性をもたらす、ガベージコレクションの状態に依存することになってしまいます。
WeakMap
キー付きコレクション
オブジェクトをキーにして値を持たせることができるらしいです
const wm = new WeakMap
var objA = {}
var objB = {}
var objC = {}
// setメソッドで値を格納
wm.set(objA, 'hoge')
wm.set(objB, 'fuga')
// getメソッドで値を取得
console.log(wm.get(objA))
// hoge
console.log(wm.get(objB))
// fuga
// setしていないものを取得する
console.log(wm.get(objC))
// undefined
// オブジェクト以外でsetする
console.log(wm.set('string', 'error'))
// Uncaught TypeError: Invalid value used as weak map key
なるほど認証機能の付いた四次元ポケットのような感じですね
const pocket = new WeakMap
var doraemon = new class Doraemon{}
var nobita = new class Nobita{}
pocket.set(doraemon, [
'どこでもドア',
'タケコプター',
])
console.log(pocket.get(doraemon))
// ["どこでもドア", "タケコプター"]
console.log(pocket.get(nobita))
// undefined
// WeakMapインスタンスをコンソールに出力して中身を確認することができる
console.log(pocket)
// WeakMap {Doraemon => Array(2)}
// __proto__: WeakMap
// [[Entries]]: Array(1)
// 0: {Doraemon => Array(2)}
// key: Doraemon {}
// value: (2) ["どこでもドア", "タケコプター"]
// length: 1
オブジェクト自体には値を持たせないでWeakMap内にそのオブジェクトに紐付いた値を持たせることができました
これがWeakMapを用いたprivateの基礎です
ポイントはsetしたオブジェクトと同じオブジェクトでgetメソッドを使わないと値を取得できないということです
privateプロパティの作り方
privateにするプロパティをWeakMapの中に閉じ込めます
手順1
WeakMapインスタンスのスコープがグローバルだと外からアクセスされてしまうので、クラス定義を返すアロー関数を即時実行させる
手順2
WeakMapインスタンスはclass定義の中でどこからでもアクセスできないといけないので、class定義より上で初期化する
手順3
継承されることを考慮して、コンストラクタ内でWeakMapの初期化(WeakMapに空のオブジェクトをsetする)処理を書くと複数回初期化されてしまうので、クラス定義より上にWeakMapへの登録と値の取得ができる関数を用意する
手順4
WeakMapにプライベートなプロパティを登録する
const Hoge = (() => {
// WeakMapインスタンス化
const wm = new WeakMap()
// 登録されているインスタンスであればその値を。
// 登録されていなければ空オブジェクトを登録しその値を返す関数
const privates = function(instance) {
return wm.get(instance) || wm.set(instance, {}).get(instance)
}
return class Hoge {
constructor() {
// プライベートプロパティ
privates(this)._privateProperty = 'プライベート'
// プライベートなgetter setter
Object.defineProperty(privates(this), 'privateProperty', {
get: function() {
return privates(this)._privateProperty
}.bind(this),
set: function(arg) {
privates(this)._privateProperty = arg
}.bind(this)
})
// プライベートメソッド
privates(this).privateMethod = function() {
return privates(this).privateProperty
}.bind(this)
}
publicMethod() {
return privates(this).privateMethod()
}
}
})()
var hoge = new Hoge
console.log(hoge)
// Hoge {}
// __proto__:
// constructor: class Hoge
// publicMethod: ƒ publicMethod()
// __proto__: Object
console.log(hoge.publicMethod())
// プライベート
const Hoge = (() => {
const wm = new WeakMap()
const privates = function(instance) {
return wm.get(instance) || wm.set(instance, {}).get(instance)
}
function Hoge() {
// プライベートプロパティ
privates(this)._privateProperty = 'プライベートプロパティ'
// プライベートなgetter setter
Object.defineProperty(privates(this), 'privateProperty', {
get: function() {
return privates(this)._privateProperty
}.bind(this),
set: function(arg) {
privates(this)._privateProperty = arg
}.bind(this)
})
// プライベートメソッド
privates(this).privateMethod = function() {
return privates(this).privateProperty
}.bind(this)
}
Hoge.prototype.publicMethod = function() {
return privates(this).privateMethod()
}
return Hoge
})()
- プライベートプロパティ
-
手順3の関数(
privates()
)によって取得した値に格納する - プライベートメソッド
-
手順3の関数(
privates()
)によって取得した値に格納する - つまりどこかしらのメソッド内に書かないといけない(今回はコンストラクタ)
- ※
bind()
しておかないとWeakMapに登録した値がthis
になる - プライベートな
getter
setter
-
手順3の関数(
privates()
)によって取得した値にObject.defineProperty()
を使う - つまりどこかしらのメソッド内に書かないといけない(今回はコンストラクタ)
- ※
bind()
しておかないとWeakMapに登録した値がthis
になる
※privates()
関数の名前は_
でもなんでもいい
見ての通りprivateなプロパティではなく、インスタンスとは別の場所にプロパティを隠している。
継承
アロー関数がクラスの定義を返しているので継承できる
また、アロー関数の中にWeakMapと値を取得する関数を閉じ込めることで、子クラスからもアクセスできないようになっている
const Parent = (() => {
const wm = new WeakMap() // Parentクラスのプライベートプロパティはここへ格納される
const privates = function(instance) {
return wm.get(instance) || wm.set(instance, {}).get(instance)
}
return class Parent {
}
})()
const Child = (() => {
const wm = new WeakMap() // Childクラスのプライベートプロパティはここへ格納される
const privates = function(instance) {
return wm.get(instance) || wm.set(instance, {}).get(instance)
}
return class Child extends Parent {
}
})()
良い点
- 外からインスタンスをコンソールに出力してもプライベートな値は見えない
- ステップ実行してそのクラスのメソッド内で
console.log(privates(this))
ってされると見られてしまう
問題点
- デバッグしにくい
- publicとprivateで書き方がだいぶ違う
- クラス作るたびにアロー関数作ってWeakMapのインスタンス用意してとやることが多い
- プライベートメソッドを定義するにはどこかのメソッド内で定義しないといけない
- ひとつインデントされて可読性が低い
- インスタンス化するたびにメソッドの定義をするので重そう
- どうにかできる方法があれば教えてください!
- privateなstaticメソッドが作れない
- どうにかできる方法があれば教えてください!
- privateなasyncメソッドを作るには更に1手間加えないといけない
const Hoge = (() => {
const wm = new WeakMap()
const privates = function(instance) {
return wm.get(instance) || wm.set(instance, {}).get(instance)
}
return class Hoge {
constructor() {
privates(this).name = 'Hoge class'
privates(this).asyncMethod = function() {
return async function() {
return privates(this).name
}.bind(this)()
}.bind(this)
}
callPrivateAsyncMethod() {
return privates(this).asyncMethod()
}
}
})()
var hoge = new Hoge
console.log(hoge.callPrivateAsyncMethod())
// Promise {<resolved>: "Hoge class"}
WeakMapにsetしたオブジェクトに、thisをbind()したasync関数を即時実行した結果を返すthisをbind()した関数を格納する(何言ってるのか分らない)
最後に
js初心者です。
間違っていることや足りないことがあれば教えていただけると嬉しいです
WeakMapを使う方法は可読性の低下とデバッグのしにくさ手間がかかるなど問題点はあるががしっかりprivateになっている
ただただめんどくさそう。