javascript typescript

プロパティ値変更を監視できるJavaScript、TypeScriptオブジェクトを作る

投稿日:

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();

-javascript, typescript
-

執筆者:

関連記事

Stripe+Java+Payment Request APIでApple Pay、Google Payを使ったテストWeb決済をしてみる

自分で作ったサービスを運用してチャリンチャリンしたい・・・エンジニアならこんな夢、一度は見たことがあるんじゃないでしょうか。 夢を実現する為、以前Stripeのcheckout.jsを使ったテストWe …

Android、iPhoneが向いている方向をWeb地図上で表現してみる(leaflet.js使用)

前回Javascriptでコンパス機能を実装して、Android、iPhoneの背面が向いている方角が数値(0~360:0が北)で分かるようになりました。 One IT Thing  9 P …

Javascript(暗号化JSライブラリ「Forge」)とp12ファイルで署名値を作成、Javaで検証する

前回、送信データの改ざんを検知する為、簡易的なセキュリティトークンであるPKCS#12形式のファイルを作成しました。 One IT Thing  10 Pockets開発用のPKCS#12フ …

Stripe + Javaでオーソリ(与信の確保)を実装する

Stripeでチャリンチャリン、サービスを開発するエンジニアにとっては夢がありますよね。 例え自分で個人的に売るものが無かったとしても、Web決済システムを構築できるノウハウを持っておけば、公的な仕事 …

Typescript 複数の継承インナークラスをI/Fで識別する

目次1 前提2 はじめに3 想定する状況4 環境・準備5 初期の実装例5.1 実行結果6 この設計で生じる課題7 課題に対する回避策7.1 実行結果8 まとめ 前提 インナークラスに型名を持たせられる …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。