One IT Thing

IT業界を楽しむ為の学習系雑記

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
-

執筆者:

関連記事

Typescript3.0以下の環境で発生する「Cannot find name ‘unknown’」に対処する

目次1 事象2 原因3 対処4 まとめ 事象 Typescript2.3.4を使っている息の長いWebシステムでnpm installをし直し、tscビルドし直したらトランスパイルエラーが発生。「un …

ionicアプリを多言語化する

目次1 目的2 環境3 手順1.インストール4 手順2.src/assets/i18nに翻訳ファイルを作成する5 手順3.app.module.tsにngx-translate関連のモジュールを登録す …

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

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

StripeとJavaで単発Web決済を一通り流してみる

会社仕事でも個人ビジネスでも、商品やサービスの対価として利用者に課金方法を提供したい時があるかと思います。 最もメジャーな決済方法であるクレジットカードで課金してもらう仕組みを導入したいところですが、 …

Angular4.4のHTTP通信処理にタイムアウトを設定をすると「timeout is not a function」エラーが発生する

目次1 事象2 原因3 対処4 まとめ 事象 Angular4.3で追加されたHttpClientModuleに移行せず、HttpModuleを使い続けているアプリで、とある理由からpackage-l …


shingo nakanishi。東京で消耗中の職歴20年越え中年ITエンジニアです。「生涯現役プログラマを楽しむ」ことができる働き方探しをライフワークにしています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。