let taro= new Person(“yamada taro”, 25, “teacher”);
taro.age = 26;
こんなコードが有ったとして、taroの年齢が25歳から26歳に設定されたことを外部から検知したい。
AngularやVueのコンポーネントが容易く行っているように、オブジェクトプロパティ値の変更を検知できるオブジェクトを作ります。
Javascriptの場合
Object.defineProperty()を使ってオブジェクトプロパティに対するsetメソッドを定義することで、プロパティ値の変更をフック出来出来るようにします。
// person-js.js
// Personクラス定義
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.observers = [];
// オブザーバを追加
this.addObservers = function(func) {
this.observers.push(func);
};
// 変更が有った際にオブザーバに通知
this.notifyObservers = (propertyName, newValue) => {
for (let i = 0; i < this.observers.length; i++) {
this.observers[i].apply(this, [propertyName, newValue]);
}
};
}
// インスタンス生成
let person = new Person("yamada taro", 25, "teacher");
// personインスタンスのageプロパティが変更されたらオブザーバに周知
Object.defineProperty(
person,
"age",
(() => {
var value;
return {
get: function() {
return this.value;
},
// person.age = ~した時に呼ばれるセッター
set: function(newValue) {
this.value = newValue;
person.notifyObservers("age", newValue);
},
enumerable: true,
configurable: true
};
})()
);
// 変更監視オブザーバをpersonインスタンスに登録
person.addObservers((propertyName, newValue) => {
console.log("プロパティ値が変更されました", propertyName, newValue);
});
// 年齢を変更
person.age = 26;
実行結果。
C:\src\node\ts\observable>node person-js
プロパティ値が変更されました age 26
コンソールダンプしているスコープでビジュアルコンポーネントの設定をしてあげれば、データ変更に併せて見た目(アイコン等)を変化させられそうです。
Typescriptの場合
Typescriptの場合はJavascriptへのトランスパイルの結果、クラスのgetter、setterがObject.defineProperty()に翻訳される為、以下で同じ効果が得られます。
taroとhanakoのインスタンスをPersonクラスから作り、プロパティ値を変更させてみます。
// person-ts.ts
export class Person {
set name(name: string) {
if (this._name == name) return;
this.notifyObserver("name", this._name, name);
this._name = name;
}
get name(): string {
return this._name;
}
set age(age: number) {
if (this._age == age) return;
this.notifyObserver("age", this._age, age);
this._age = age;
}
get age(): number {
return this._age;
}
set job(job: string) {
if (this._job == job) return;
this.notifyObserver("job", this._job, job);
this._job = job;
}
get job(): string {
return this._job;
}
public category: string = "";
private observers: Function[] = [];
constructor(
private _name: string,
private _age: number,
private _job: string
) {}
public addObserver(observer: Function) {
this.observers.push(observer);
}
private notifyObserver(propertyName: string, oldValue: any, newValue: any) {
for (let i = 0; i < this.observers.length; i++) {
let o = this.observers[i];
o.apply(this, [propertyName, oldValue, newValue]);
}
}
public toJson(): string {
let ret = {
name: this.name,
age: this.age,
job: this.job
};
return JSON.stringify(ret);
}
}
export class Main {
constructor() {
let taro = new Person("yamada taro", 25, "teacher");
// taroのプロパティ値変更を監視
taro.addObserver(
(propertyName: string, oldValue: any, newValue: any) => {
this.dump(taro, propertyName, oldValue, newValue);
}
);
let hanako = new Person("sato hanako", 19, "student");
// hanakoのプロパティ値変更を監視
hanako.addObserver(
(propertyName: string, oldValue: any, newValue: any) => {
this.dump(hanako, propertyName, oldValue, newValue);
}
);
console.log("--------コンストラクタによる初期値");
console.log(taro.toJson());
console.log(hanako.toJson());
console.log("--------taroとhanakoのプロパティ値を変更してみる");
taro.job = "engineer";
taro.age = 26;
hanako.name = "yamada hanako";
hanako.age = 20;
console.log("--------プロパティ値変更後");
console.log(taro.toJson());
console.log(hanako.toJson());
}
private dump(
person: Person,
propertyName: string,
oldValue: any,
newValue: any
): void {
console.log(
person.name,
"変更の有ったプロパティ = ",
propertyName,
" : ",
oldValue,
" → ",
newValue
);
}
}
new Main();
実行結果。
C:\src\node\ts\observable>tsc --init
message TS6071: Successfully created a tsconfig.json file.
C:\src\node\ts\observable>tsc --project ./tsconfig.json
C:\src\node\ts\observable>node person-ts
--------コンストラクタによる初期値
{"name":"yamada taro","age":25,"job":"teacher"}
{"name":"sato hanako","age":19,"job":"student"}
--------taroとhanakoのプロパティ値を変更してみる
yamada taro 変更の有ったプロパティ = job : teacher → engineer
yamada taro 変更の有ったプロパティ = age : 25 → 26
sato hanako 変更の有ったプロパティ = name : sato hanako → yamada hanako
yamada hanako 変更の有ったプロパティ = age : 19 → 20
--------プロパティ値変更後
{"name":"yamada taro","age":26,"job":"engineer"}
{"name":"yamada hanako","age":20,"job":"student"}
taroは教師からエンジニアに転職し、hanakoは苗字がyamadaになったことがプロパティ値変更監視から検知出来ました。
まとめ
AngularやVueを使っていればモデル値変更を監視してviewを変えることが簡単にできるけど、そもそも表示に使っているのがAnagularやVueのコンポーネントじゃない、みたいなケースは結構あります。
GoogleMapsのマーカとか外部ライブラリによるビジュアルオブジェクトの表示をデータモデル変更に併せて変化させたい時はJavascriptオブジェクトのプロパティ監視をObject.defineProperty()で行い、setをフックして変更イベントを通知してあげれば先進的なフレームワークと似たようなことが出来そうです。
補足
上記TypescriptのJSへのtscトランスパイル結果。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Person = /** @class */ (function () {
function Person(_name, _age, _job) {
this._name = _name;
this._age = _age;
this._job = _job;
this.category = "";
this.observers = [];
}
Object.defineProperty(Person.prototype, "name", {
get: function () {
return this._name;
},
set: function (name) {
if (this._name == name)
return;
this.notifyObserver("name", this._name, name);
this._name = name;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Person.prototype, "age", {
get: function () {
return this._age;
},
set: function (age) {
if (this._age == age)
return;
this.notifyObserver("age", this._age, age);
this._age = age;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Person.prototype, "job", {
get: function () {
return this._job;
},
set: function (job) {
if (this._job == job)
return;
this.notifyObserver("job", this._job, job);
this._job = job;
},
enumerable: true,
configurable: true
});
Person.prototype.addObserver = function (observer) {
this.observers.push(observer);
};
Person.prototype.notifyObserver = function (propertyName, oldValue, newValue) {
for (var i = 0; i < this.observers.length; i++) {
var o = this.observers[i];
o.apply(this, [propertyName, oldValue, newValue]);
}
};
Person.prototype.toJson = function () {
var ret = {
name: this.name,
age: this.age,
job: this.job
};
return JSON.stringify(ret);
};
return Person;
}());
exports.Person = Person;
var Main = /** @class */ (function () {
function Main() {
var _this = this;
var taro = new Person("yamada taro", 25, "teacher");
// taroのプロパティ値変更を監視
taro.addObserver(function (propertyName, oldValue, newValue) {
_this.dump(taro, propertyName, oldValue, newValue);
});
var hanako = new Person("sato hanako", 19, "student");
// hanakoのプロパティ値変更を監視
hanako.addObserver(function (propertyName, oldValue, newValue) {
_this.dump(hanako, propertyName, oldValue, newValue);
});
console.log("--------コンストラクタによる初期値");
console.log(taro.toJson());
console.log(hanako.toJson());
console.log("--------taroとhanakoのプロパティ値を変更してみる");
taro.job = "engineer";
taro.age = 26;
hanako.name = "yamada hanako";
hanako.age = 20;
console.log("--------プロパティ値変更後");
console.log(taro.toJson());
console.log(hanako.toJson());
}
Main.prototype.dump = function (person, propertyName, oldValue, newValue) {
console.log(person.name, "変更の有ったプロパティ = ", propertyName, " : ", oldValue, " → ", newValue);
};
return Main;
}());
exports.Main = Main;
new Main();