インスタンスのプロパティに対して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