JavaのSerializableのようなものをJavaScriptで!
デコレータ使ってJSONのstringify/parseでオブジェクトを簡単に復元できるようにしてみる。
toJSON
前知識。
JavaScriptの仕様として、オブジェクトにtoJSONを実装しておくとJSON.stringifyの時に参照してくれる。
別のオブジェクト内部にあってもOK。
var obj = {
a: 1,
b: {
c: 1,
toJSON: function(){ return 'hoge'; }
}
};
JSON.stringify(obj); // => "{"a":1,"b":"hoge"}"
これオライリーのJavaScript本読むまで知らなかった。みんなちゃんと定義書読もうね。
parse
また、JSON.parseにも第二引数に関数を設定して処理を挟める。
JSON.parse('{"a":1,"b":2}', function(key,value) {
if(key === 'b') {
return 'piyo';
} else {
return value;
}
}); // => Object {a: 1, b: "piyo"}
前知識ここまで。
Decoratorでオブジェクト復元
TypeScriptのDecorator使ったらオブジェクト簡単に復元できんじゃね?ってことで、
Decorator使って、クラスにtoJSONを生やすものを書いてみた。
追記: コンストラクタの格納方法を修正。
serializable.ts
const constructors = {}; // コンストラクタ格納
// class用decorator (toJSONを生やす)
export const serializable = (target: any) => {
constructors[target.name] = target;
target.prototype.toJSON = function(key) {
if (key === '__serializeValue') { // 入れ子ループ防止用
return this;
} else {
// デシリアライズに必要な情報を埋める
return {
__serializeName: target.name,
__serializeValue: this
};
}
};
};
// JSON.parse用関数
export const deserialize = (k:string, v:any) => {
// 関係ないオブジェクトは通常処理
if(!v.__serializeName) return v;
if (constructors[v.__serializeName].fromJSON){
// fromJSONが定義されていればそれを利用
return constructors[v.__serializeName].fromJSON(v.__serializeValue);
} else {
// 定義されていなければ無引数でコンストラクタ実行して値設定
const obj = new constructors[v.__serializeName]();
for (let key in v.__serializeValue) {
if (v.__serializeValue.hasOwnProperty(key)) {
obj[key] = v.__serializeValue[key];
}
}
return obj;
}
};
こうやって使う。
import {serializable, deserialize} from './serializable'
@serializable // シリアライズ可能にする!
class Hoge{
piyo: number;
fuga: number;
piyofuga() {
return this.piyo + this.fuga;
}
}
const hoge = new Hoge();
hoge.piyo = 3;
hoge.fuga = 2;
console.log(hoge.piyofuga()); // => 5
// 文字列化
const json = JSON.stringify(hoge);
console.log(json);
// => {"__serializeName":"Hoge","__serializeValue":{"piyo":3,"fuga":2}}
// 復元
const obj = JSON.parse(json, deserialize);
console.log(obj.piyofuga()); // => 5
ただしこの場合、無引数でコンストラクタが実行されてしまう。
なので、静的関数fromJSONがあればそれを使うようにしてある。
import {serializable, deserialize} from './serializable'
@serializable
class Hoge{
constructor(piyo: number, fuga:number) {
this.piyo = piyo;
this.fuga = fuga;
console.log(`Created! piyo:${piyo} fuga:${fuga}`);
}
// JSONからの復元方法を指定
static fromJSON(json: {piyo: number, fuga:number}):Hoge {
return new Hoge(json.piyo, json.fuga);
}
}
const hoge = new Hoge(3, 2); // => "Created! piyo:3 fuga:2"
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:3 fuga:2"
// fromJSONがない場合
delete Hoge.fromJSON;
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:undefined fuga:undefined"
ちなみに、別段Decorator使わなくても普通に呼んでもいい。
むしろこっちのほうが汎用的かもしれない。(ええ...)
class Hoge{
piyo: number;
fuga: number;
piyofuga() {
return this.piyo + this.fuga;
}
}
serializable(Hoge); // toJSON生やす
状態保存に使えたりしないかな。