LoginSignup
2
3

More than 5 years have passed since last update.

メモリにやさしくインスタンスの動的プロパティ名にgetter/setterを

Last updated at Posted at 2018-03-18

インスタンスのプロパティに対してgetter/setterを使いたい時は以下のコードで事足ります。

class SomeClass {
    constructor(arg){
        this.someProp = arg;
    }
    get someProp(){
        console.log("getter");
        return "value of someProp";
    }
    set someProp(val){
        console.log("setter", val);
    }
}

しかし、そうそうあることではありませんがプロパティ名が動的な時はどうすればいいのでしょうか。
答えは以下の通りです。

const propName = "hogefuga";
class SomeClass {
    constructor(arg){
        this[propName] = arg;
    }
    get [propName](){
        console.log("getter");
        return `value of ${propName}`;
    }
    set [propName](val){
        console.log("setter", val);
    }
}

クラス構文にもcomputed propertyは使えるようなのでこれでよいでしょう。

インスタンスごとに関数を生成せず、どのインスタンスも同じ関数を参照するのでインスタンスを大量に生成してもメモリを圧迫しにくくなっています。getter/settterの中でのthisがインスタンスを指します。

また、クラスのprototypeにgetter/setterを定義するという方法もあります。(後述)

「変更があるとこの関数を呼ぶ」というプロパティを作りたいとき、ひとつなら以下のスクリプトで実現できます。

const propName = "hogefuga";

const SomeClass = (() => {
    const map = new WeakMap();
    class SomeClass {
        constructor(){
            this.listeners = {
                change: []
            };
        }
        addEventListener(type, fn){
            this.listeners[type].push(fn);
        }
        removeEventListener(type, fn){
            const arr = this.listeners[type];
            arr.splice(arr.indexOf(fn), 1);
        }
        get [propName](){
            return map.get(this);
        }
        set [propName](val){
            map.set(this, val);
            this.listeners.change.forEach(fn => fn());
        }
    }
    return SomeClass;
})();
const foobar = new SomeClass();

foobar.hogefuga = 10;
console.log(foobar.hogefuga); // 10

const listener1 = () => console.log("listener1");
foobar.addEventListener("change", listener1);
foobar.hogefuga = 20;
// listener1

foobar.addEventListener("change", () => console.log("listener2"));
foobar.hogefuga = 30;
// listener1
// listener2

foobar.removeEventListener("change", listener1);
foobar.hogefuga = 40;
// listener2

しかし、これが複数になるとどうでしょう。

const propNames = ["hoge", "fuga"];

const SomeClass = (() => {
    const maps = {};
    propNames.forEach(n => maps[n] = new WeakMap());

    class SomeClass {
        constructor(){
            this.listeners = {
                change: []
            };
        }
        addEventListener(type, fn){
            this.listeners[type].push(fn);
        }
        removeEventListener(type, fn){
            const arr = this.listeners[type];
            arr.splice(arr.indexOf(fn), 1);
        }
        get [propNames[0]](){
            return maps[propNames[0]].get(this);
        }
        set [propNames[0]](val){
            maps[propNames[0]].set(this, val);
            this.listeners.change.forEach(fn => fn());
        }
        get [propNames[1]](){
            return maps[propNames[1]].get(this);
        }
        set [propNames[1]](val){
            maps[propNames[1]].set(this, val);
            this.listeners.change.forEach(fn => fn());
        }
    }
    return SomeClass;
})();

//略

なんとも冗長ですしこれではpropNamesの長さが変わるときお手上げです。そこでObject.definePropertiesを使い、prototypeにgetter/setterを定義します。

const propNames = ["hoge", "fuga"];

const SomeClass = (() => {
    class SomeClass {
        constructor(){
            this.listeners = {
                change: []
            };
        }
        addEventListener(type, fn){
            this.listeners[type].push(fn);
        }
        removeEventListener(type, fn){
            const arr = this.listeners[type];
            arr.splice(arr.indexOf(fn), 1);
        }
    }

    // ↓ ここからがポイント ↓
    const maps = {};
    propNames.forEach(n => maps[n] = new WeakMap());

    const o = {};
    propNames.forEach(n => o[n] = {
        get(){
            return maps[n].get(this);
        },
        set(val){
            maps[n].set(this, val);
            this.listeners.change.forEach(fn => fn());
        }
    })
    Object.defineProperties(SomeClass.prototype, o);
    // ↑ ここまでがポイント ↑

    return SomeClass;
})();
const foobar = new SomeClass();

foobar.hoge = 10;
foobar.fuga = "abc";

console.log(foobar.hoge, foobar.fuga); // 10 "abc"

const listener1 = () => console.log("listener1");
foobar.addEventListener("change", listener1);

foobar.hoge = 20;
// listener1

foobar.addEventListener("change", () => console.log("listener2"));

foobar.fuga = "def";
// listener1
// listener2

foobar.hoge = 30;
// listener1
// listener2

foobar.removeEventListener("change", listener1);

foobar.fuga = 40;
// listener2
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3