ガチガチに固めた、静的な言語のようなクラスをJavaScript(TypeScript+ES6+ES7)で作ってみるシリーズ。
クラスメンバーの追加を阻止する
JavaやC#などのクラスは基本的には定義後にメンバープロパティを追加できない。TypeScriptも同様だが、あくまでもコンパイル時チェックであって、実行時は普通のjsなので追加できてしまう。
ES5の機能にObject.preventExtensions()
,Object.seal()
,Object.freeze()
があり、これらを使うとクラスメンバープロパティの追加を阻止できる1。
"use strict";
class A{
constructor(){
Object.preventExtensions(this);
}
prop = 1;
}
//実行
var a = new A();
a.prop2 = 100; //error!:Uncaught TypeError: Can't add property prop2, object is not extensible
上記のコードで2はコンストラクタでObject.preventExtensions()
の対象にクラスインスタンス(this
)を指定する事ですべてのクラスインスタンスへのプロパティ追加を阻止している。
なお、この方法ではTypeScriptのコンパイル時チェックをすり抜ける、ブラケットアクセスであっても阻止できる。
a["prop2"] = 100; //runtime error!
3つのメソッドの違い
- プロパティの値を変更・削除してもいい
Object.preventExtensions()
- ただし、プロパティが関数の場合は再割当禁止
- プロパティの値を変更してもいいが削除はダメ
Object.seal()
- プロパティの値の変更もダメ
Object.freeze()
//base
class A{
prop = 1;
constructor(){
/* Object.*(this); */
}
func(){}
}
var a = new A();
//Object.preventExtensions()
a.prop = 1000; //OK
delete a.prop; //OK
a.func = () => {}; //NG
a.prop = () => {}; //OK (TSではNG)
//Object.seal()
a.prop = 1000; //OK
delete a.prop; //NG
a.func = () => {}; //NG
a.prop = () => {}; //OK (TSではNG)
//Object.freeze()
a.prop = 1000; //NG
delete a.prop; //NG
a.func = () => {}; //NG
a.prop = () => {}; //NG
JavaやC#などのクラスにはpreventExtensions()
やseal()
を指定した時がイメージが近い。freeze()
は全てがreadonlyなので、Javaでいえば全部のメンバー変数にfinal
/C#で言えば全部のメンバー変数にreadonly
を指定したのと近い。
デコレータでアクセス修飾子風に指定する
上記で無事実現できたが、1行とは言え毎回コンストラクタ内で指定するのは他のコードに混じって意味がわかりにくい。デコレータをつかうことで、アクセス修飾子風に記述することができる。
@seal class A{
constructor(){}
prop = 1;
}
デコレータ自体の定義は以下。コンストラクタを置き換えるためやや長いが一度定義してしまえばあとは簡単。
function seal<T extends Function>(t:T):T|void{
let base = t;
function construct(constructor:T, args:any[]) {
let c: any = function () {
return constructor.apply(this, args);
};
c.prototype = constructor.prototype;
return new c();
}
let func: any = (...args:any[])=>{
let a = construct(base, args);
Object.seal(a); //ここで指定
return a;
};
func.prototype = base.prototype;
return func;
}
var a = new A();
a.prop = 10;
a.prop2 = 10; //error!
上記コードはES7のデコレータを使うので、TypeScript1.5.3以降、--experimentalDecorators
オプションを有効にすることで使える。型指定を省けば、Babelでも有効。
※なお、@sealed
としなかったのはC#のsealed
とかぶるため。C#のsealed
は継承不可の指定のため意味合いが異なる。
注意点
- 入れ子のプロパティ、つまりプロパティの子プロパティには適応されない
- 入れ子まで指定したい場合は、子や孫までずっと走査して指定する必要がある
- デコレータの場合は、継承したクラスにも同じくデコレータを指定する必要がある
- コンストラクタ内で指定の場合は継承するだけでOK
- ES6の範囲内だけでやる場合、コンストラクタ内で変数プロパティを宣言後に
Object.seal(this)
とする必要がある- 後からやらないと一切追加できなくなるため