2

I'm currently trying to write a class that defines a generic getter and setter for all properties of the object. The closest thing I have right now is functions named get and set for the class as a whole which takes a property name and returns it if it exists.

I've tried to use the computed property names option for dynamically defining a getter, but I'm getting errors on either giving the getter arguments, or errors about prop being undefined. Is there any way to define a pair that works for both obj.prop and obj['prop'] that doesn't require writing them for every property in a class?

Current code for reference:

class Holder {
    constructor(def="the door"){
    this.holds=def
  }
  // Generic getter for every possible property
  get (prop) {
    if(prop in this){
        return this[prop];
    } else {
        return `No property found named '${prop}'`;
    }
  }
  // Generic setter, performs magic that sets `changed` as well.
  set (prop, value) {
        if(prop in this){
        this[prop] = value;
        this.changed = true;
    }
  }
}

const hodor = new Holder();
console.log(hodor.holds); // Expected: "the door"
console.log(hodor.derp); // Expected: "No property found named 'derp'"
RivenSkaye
  • 440
  • 3
  • 13

2 Answers2

3

So if you run the code above you'll find it's not actually working. The console.log(hodor.holds) is going directly to the underlying this.holds instance property.

A quick search on StackOverflow led me to this Is it possible to implement dynamic getters/setters in JavaScript?. I've modified your code to use this Proxy approach and this now works correctly:

class Holder {
    constructor(def = 'the door') {
        this.holds = def;

        return new Proxy(this, {
            get(target, name, receiver) {
                if (name in target) {
                    return target[name];
                } else {
                    return `No property found named '${name}'`;
                }
            },
            set(target, name, receiver) {
                if (name in target) {
                    target[name] = receiver;
                    target.changed = true;
                }
            },
        });
    }
}

const hodor = new Holder();
console.log(hodor.holds); // Logs: the door
console.log(hodor.derp); // Logs: "No property found named 'derp'"
hodor.holds = 'bar';
console.log(hodor.holds); // Logs "bar"
Ian
  • 33,605
  • 26
  • 118
  • 198
  • 1
    I noticed it doesn't work as intended, but SO convention taught me to post the broken code either way. Will test this out for sure – RivenSkaye Aug 13 '21 at 08:43
  • Does exactly what it needs to do in the most concise way possible. The `receiver` arg from `get` can safely be dropped as well since there's no need for it AFAICT and testing it out seems to yield the same result. – RivenSkaye Aug 13 '21 at 08:53
  • 1
    @RivenSkaye yes you're correct the `receiver` can be dropped on the `get`. Just wanted to leave it there so you know about it though :) I'd personally rename `reciever` to `value` on the set actually. – Ian Aug 13 '21 at 08:59
  • 1
    current implementation uses `obj`, `prop` and `value`. Thanks again – RivenSkaye Aug 13 '21 at 09:00
2

Is there any way to define a pair that works for both obj.prop and obj['prop'] that doesn't require writing them for every property in a class?

Two options for you:

  1. In the constructor, build the accessors in a loop over an array of the names of the properties you want to have accessors. This is creating them for each property, but not writing them for each property. :-)

  2. Or use a Proxy, but you may not want the overhead. With a Proxy object, you can intercept the get and set actions regardless of what property is being gotten/set.

But in both cases, you'll need a place to put the actual values the accessors access. That could be a WeakMap keyed by Holder instances, or a private property (either truly private or pseudo-private), or something else.

Here's a quick sketch of #1 with a WeakMap:

const holderData = new WeakMap();
class Holder {
    constructor(def = "the door") {
        // Create an object to hold the values for this instance
        holderData.set(this, Object.create(null));
        this.def = def;
    }
}
for (const name of ["def", "x", "y", "z"]) {
    Object.defineProperty(Holder.prototype, name, {
        get() {
            console.log(`(Getting "${name}")`);
            return holderData.get(this)[name];
        },
        set(value) {
            console.log(`(Setting "${name}" to "${value}")`);
            holderData.get(this)[name] = value;
        }
    });
}

const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);

When a Holder instance is no longer reachable, it becomes eligible for garbage collection, and the same happens to the object in the WeakMap.

Here's a quick sketch of #2, also with a WeakMap although you could just use the target object (I think when I wrote this I hadn't had enough coffee yet):

const holderData = new WeakMap();
class Holder {
    constructor(def = "the door") {
        // Create an object to hold the values for this instance
        holderData.set(this, Object.create(null));
        // Create the proxy
        const proxy = new Proxy(this, {
            get(target, propKey, receiver) {
                if (typeof propKey === "string") { // Or whatever check you want for
                                                   // the properties you want to handle
                    console.log(`(Getting ${propKey.toString()})`);
                    return holderData.get(target)[propKey];
                }
                // Default handling
                return Reflect.get(target, propKey, receiver);
            },
            set(target, propKey, value, receiver) {
                if (typeof propKey === "string") { // Or whatever
                    console.log(`(Setting "${propKey.toString()}" to "${value}")`);
                    holderData.get(target)[propKey] = value;
                    return true;
                }
                // Default handling
                return Reflect.set(target, propKey, receiver, value);
            }
        });
        proxy.def = def;
        return proxy;
    }
}

const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);

Here's that same thing using the target object to hold the values:

class Holder {
    constructor(def = "the door") {
        this.def = def;
        // Return the proxy
        return new Proxy(this, {
            get(target, propKey, receiver) {
                if (typeof propKey === "string") { // Or whatever check you want for
                                                   // the properties you want to handle
                    console.log(`(Getting ${propKey.toString()})`);
                    return target[propKey];
                }
                // Default handling
                return Reflect.get(target, propKey, receiver);
            },
            set(target, propKey, value, receiver) {
                if (typeof propKey === "string") { // Or whatever
                    console.log(`(Setting "${propKey.toString()}" to "${value}")`);
                    target[propKey] = value;
                    return true;
                }
                // Default handling
                return Reflect.set(target, propKey, receiver, value);
            }
        });
    }
}

const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Does what it needs to do very well, but the `WeakMap` is not a requirement when using the proxy. Still a great answer! – RivenSkaye Aug 13 '21 at 08:56
  • @RivenSkaye - I didn't say `WeakMap` was a requirement, just that you need *somewhere* to put the values. :-) But you're right, that can just be the target object! – T.J. Crowder Aug 13 '21 at 09:45