JavaScriptには古くから自然なclass構造が在る。ECMAScript2015では簡単な構文糖が附いた。
多くの他の電算機言語に於けるclassには 1. 委讓delegationの役目 2. 型表示の役目 3. 識別子のscopeを限る役目 が有る。一方JavaScriptのclassには 1. 委讓delegationの役目 と 2. 型表示の役目 だけである。JavaScriptのオブジェクトは単に連想配列に過ぎないので、「メソッド」や「フィールド」の參照を妨げるものは何もない。連想配列はアクセスする爲の構造だ。型表示はinstanceofで確認できる。3. 識別子のscopeを限る役目 を担うのはclassではなく函數scope function () { var v; }
による。最近はletによるblock scopeも増えた。letを使ふ機會は少ないが便利だ。
多くの言語のclassに有るprivateとprotectedと云ふ 3. 識別子のscopeを限る役目 は、classの継承と結びついてゐる爲にJavaScriptには (未だ) 存在しない (class構文糖ができたのに続き、規格提案は在る Encapsulated private state for objects)。privateなメソッドは簡単に実現できることは古くから知られてゐたが、あとはあまり例がない。特にprotectedは本質的に困難だ。
此の記事では過去の方法を網羅的に記述しない。今回採用した方法のみ書く。最下部のcodeは動作testをしてある。あとは目視。
JavaScriptのclass
JavaScriptには古くから自然なclass構造が在る。prototypeへの委讓だ。
継承も簡単に実現できる。
cf. JavaScriptで「普通に」継承する - c4se記:さっちゃんですよ☆
ECMAScript2015では、よい構文糖ができた。
function Momonga() { this.v = 2; } Momonga.prototype.hello = function () { }; var m = new Momonga(); m.v; m.hello();
は
class Momonga { constructor() { this.v = 2; } hello() { } } var m = new Momonga(); m.v; m.hello();
と書ける。継承もextendsとsuperで簡単に書け、様々に溢れた不自然な方法を防止できるとおもふ。
public/private/protected/staticの定義
言語によりpublic/private/protected/staticの定義が違ふので、此處で作るものを定義しておく。JavaやC#に近い定義を採用する。
- publicとはクラス或いはインスタンスを參照できるならば常にアクセスできるもの。
- privateとはそのクラス定義の中からのみアクセスできるもの。
- protectedとはそのクラス定義と、そのクラスを継承したクラス定義の中からのみアクセスできるもの。
- staticとはそのclassのインスタンスを必要とせずにアクセスできるもの。
JavaScriptに於いてはだいたい次のやうになる。
- publicとはクラス或いはインスタンスを參照できるならば常にアクセスできるもの。
- privateとはそのクラスのconstructorやprototype等からアクセスできるもの。
- protectedとはそのクラスと、そのクラスを継承したクラスの、constructorやprototype等からアクセスできるもの。
- staticとはそのclassのインスタンスを必要とせずにアクセスできるもの。
もう少し制限はきつくなるが、だいたい此のやうなものを作る。
public, public static
標準のJavaScriptだ。
class Momonga { publicHello() { } } Momonga.publicStaticHello = function () { }; new Momonga().hello(); Momonga.publicStaticHello();
publicもpublic staticも、extendsにより継承される。
private, private static
フィールド (値) ではなくメソッド (函數) に限れば、古くからprivateは実現できた。函數scopeで区切ってやればよいだけだ。
var Momonga = (() => { var privateStaticValue; function privateHello() { } function privateStaticHello() { } class Momonga { constructor() { privateStaticValue; privateHello.call(this); privateStaticHello(); } } return Momonga; })();
まぁletでもできるが。
{ let privateStaticValue; let privateHello = function () { }; let privateStaticHello = () => { }; var Momonga = class Momonga { constructor() { privateStaticValue; privateHello.call(this); privateStaticHello(); } } }
制限は有って、classによってではなく字句的lexicalにscopeが区切られるので、例へば別のfileでprototypeを追加しやうとしたときprivateは参照できない。あまり困らないとおもふ。
private staticなメソッドやフィールドは簡単で、privateなメソッドもbind (call, apply)、或いはthisを引數渡しすれば簡単に作れる。しかしprivateなフィールドは長く実現法が無かった。_
にはアクセスしない等の最低限の紳士協定を立てるか、メモリーリークを起こす方法しか無かった。だが、かなり前にはなるが、GCに影響しないWeakMapがJavaScriptに追加されたので、簡単に作れるやうになった。
{ let privateStore = new WeakMap(); var Momonga = class Momonga { constructor(v) { privateStore.set(this, {v: v}); privateStore.get(this).v; } } }
thisをWeakMapのキーにするのが肝で、これでGCされる。privateStoreを保持するclosure自体はMomongaクラスへの參照が消滅するまで消えないので、WeakMapでなくハッシュテーブルや配列ではprivateStoreの内容は決してGCされなくなることに注意せよ。JavaScriptにdestructorは未だ無いので、適切にデータを消すことはできない。
protected
protectedを実装するには本質的に問題が在る。JavaScriptでは參照を遮る仕組みは字句的laxicalなclosure (函數scopeと、letによるblock scope) しか無い。字句的scopeの外から見えるデータは、全ての者から見える。
先のWeakMapは面白くて、WeakMapではキーを一覧する方法が無いから、キーを知らないデータは参照できない。或るインスタンスは、他のインスタンスを知らないので、他のインスタンスのデータを参照できない。
ここで作る必要が在るのは、字句的なscopeではなく、認證である。秘密の情報があり、この情報を基にしなければアクセスできないか、異なる情報を渡されればerrorになる。継承先が秘密の情報を生成し、親インスタンスをこの情報を基に初期化する。
'use strict'; import uuid from 'node-uuid'; { let privateStore = new WeakMap(); let protectedStore = new WeakMap(); var Momonga = class Momonga { constructor(v, _id) { _id = _id || uuid.v4(); privateStore.set(this, {_id: _id}); protectedStore.set(this, {}); Momonga.prototype._setProtetedProp.call(this, _id, 'protectedHelloV', `protected hello ${v}`); } _hasProtectedProp(_id, k) { if (privateStore.get(this)._id !== _id) { throw new Error("protected props can't access publicly."); } return protectedStore.get(this).hasOwnProperty(k); } _getProtectedProp(_id, k) { if (privateStore.get(this)._id !== _id) { throw new Error("protected props can't access publicly."); } return protectedStore.get(this)[k]; } _setProtetedProp(_id, k, v) { if (privateStore.get(this)._id !== _id) { throw new Error("protected props can't access publicly."); } protectedStore.get(this)[k] = v; } protectedHello(_id) { if (privateStore.get(this)._id !== _id) { throw new Error("protected props can't access publicly."); } return `protected hello ${this.v}`; } hello() { var _id = privateStore.get(this)._id; this.protectedHello(_id); this._getProtectedProp(_id, 'protectedHelloV'); this._setProtetedProp(_id, 'k', v); this._getProtectedProp(_id, 'k'); } } }
今回は、ランダムに生成した正しいIDを示せなければerrorを返す方法を採った。protectedといふより、キーを指定して識別子のスロットを作ってゐるだけで、それを継承関係の中で利用してゐるだけだ。インスタンス化する時に外からIDを指定してしまへば、IDが既知になる爲に自由にアクセスできるやうになってしまふが、多段の継承まで考慮して、外からIDを指定して無理矢理つくることをうまく防ぐ方法を見つけられない。秘密情報をだう生成し、共有するかといふ問題だ。鍵交換でもやるか…。
より詳細は下記のcodeを見れば、読み取れるとおもふ。よりよい方法を探してゐるところでもある。
protected staticは面倒なのでやめる。同じ方法で実装できるのは明らかだ。もっと抽象化してからやりたいと思ふ。
追記20160104
new.targetが使へると簡単なんだけどナー。どの函數のnewとして呼ばれてゐるか判別でき、thisを使はないのでsuperより前に書けるので、多段継承のときにも、自分がnewとして呼ばれてゐるのに秘密情報が外から與へられてゐればエラーを返せる。
cf. new.target - JavaScript | MDN
cf. new.target (ES6) - Chrome Platform Status
readonly
おまけでreadonlyもやる。JavaScriptに機能が用意されてゐる。
class Momonga { get readonlyHello { } }
Object.defineProperty() だ。
constructorで設定するreadonlyをやりたければ、privateを併用すればよい。
{ let privateStore = new WeakMap(); var Momonga = class Momonga { constructor() { privateStore.set(this, {v: 2}); } get v { return privateStore.get(this).v; } } }