You can do this with Proxy objects (MDN | spec). Proxies let you create objects that are true proxies (facades) for other objects. Here's a simple example that turns any property values that are strings to all caps on retrieval:
const original = { "foo": "bar" };
const proxy = new Proxy(original, {
get(target, name, receiver) {
let rv = target[name];
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
});
console.log(`original.foo = ${original.foo}`); // "bar"
console.log(`proxy.foo = ${proxy.foo}`); // "BAR"
The output of the above is:
original.foo = bar
proxy.foo = BAR
Operations you don't override have their default behavior. In the above, all we override is get, but there's a whole list of operations you can hook into (getting a property, setting a property, looking up the list of existing properties, and others).
In the get handler function's arguments list:
target is the object being proxied (original, in our case).
name is (of course) the name of the property being retrieved.
receiver is either the proxy itself or something that inherits from it. In our case, receiver is === proxy, but if proxy were used as a prototype, receiver could be a descendant object, hence it being on the function signature (but at the end, so you can readily leave it off if, as with our example above, you don't actually use it).
This lets you create an object with the catch-all getter and setter feature you want:
const obj = new Proxy({}, {
get(target, name) {
if (!(name in target)) {
console.log(`Getting non-existent property "${name}"`);
return undefined;
}
return target[name];
},
set(target, name, value) {
if (!(name in target)) {
console.log(`Setting non-existent property '${name}', initial value: ${value}`);
}
target[name] = value;
}
});
console.log("[before] obj.foo = " + obj.foo);
obj.foo = "bar";
console.log("[after] obj.foo = " + obj.foo);
The output of the above is:
Getting non-existent property 'foo'
[before] obj.foo = undefined
Setting non-existent property 'foo', initial value: bar
[after] obj.foo = bar
Note how we get the "non-existent" message when we try to retrieve foo when it doesn't yet exist, and again when we create it, but not subsequently.