JS Define getter for every property of a class - javascript

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'"

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"

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:
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. :-)
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}`);

Related

How to observe changes to contents of an object's array property exposed through setter-getter or Proxy

Using getter/setter
I'm creating an IIFE like below. It returns getters and setters to an array variable stored internally. I wish to intercept changes made to that array - the console.log is intended to indicate that in the setter below.
const a = (function() {
let arr = [];
return {
get arr() {return arr},
set arr(v) {
console.log("new arr", v);
arr = v;
},
}
})();
This works fine if I completely reassign arr, e.g. a.arr = [1, 2].
But it doesn't intercept changes made to contents of the array, e.g. a.arr.push(3) or a.arr.shift().
Looking for any ideas on how to intercept these content changes.
Using Proxy
This is an alternate attempt using the new Proxy object:
a = (function() {
let details = {
arr: []
}
function onChangeProxy(object, onChange) {
const handler = {
apply: function (target, thisArg, argumentsList) {
onChange(thisArg, argumentsList);
return thisArg[target].apply(this, argumentsList);
},
defineProperty: function (target, property, descriptor) {
Reflect.defineProperty(target, property, descriptor);
onChange(property, descriptor);
return true;
},
deleteProperty: function(target, property) {
Reflect.deleteProperty(target, property);
onChange(property, descriptor);
return;
}
};
return new Proxy(object, handler);
};
return onChangeProxy(details, (p, d) => console.log(p, d));
})();
The problem remains the same. Still unable to observe changes made to the contents of a.arr using anything from a.arr[0] = 1 to a.push(3).
Update: The elegant solution (courtesy Kris Pruden and Sindre Sorhus)
The solution is based on this commit by Kris on Sindre's 'on-change' library.
Explanation of the solution, by Kris:
In the set trap, my goal is to determine if the value provided is a Proxy produced by a previous call to the get trap. If it is such a Proxy, any property access will be intercepted by our own get trap. So, when I access value[proxyTarget] our get trap will be invoked, which is coded to return target when property === proxyTarget (line 46). If the value passed to set is not an on-change-created Proxy, then value[proxyTarget] is undefined.
Full code of the solution:
(object, onChange) => {
let inApply = false;
let changed = false;
function handleChange() {
if (!inApply) {
onChange();
} else if (!changed) {
changed = true;
}
}
const handler = {
get(target, property, receiver) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
const value = Reflect.get(target, property, receiver);
// Preserve invariants
if (descriptor && !descriptor.configurable) {
if (descriptor.set && !descriptor.get) {
return undefined;
}
if (descriptor.writable === false) {
return value;
}
}
try {
return new Proxy(value, handler);
} catch (_) {
return value;
}
},
set(target, property, value) {
const result = Reflect.set(target, property, value);
handleChange();
return result;
},
defineProperty(target, property, descriptor) {
const result = Reflect.defineProperty(target, property, descriptor);
handleChange();
return result;
},
deleteProperty(target, property) {
const result = Reflect.deleteProperty(target, property);
handleChange();
return result;
},
apply(target, thisArg, argumentsList) {
if (!inApply) {
inApply = true;
const result = Reflect.apply(target, thisArg, argumentsList);
if (changed) {
onChange();
}
inApply = false;
changed = false;
return result;
}
return Reflect.apply(target, thisArg, argumentsList);
}
};
return new Proxy(object, handler);
};
This has solved my problem, without resorting to the hack of checking for array modifying methods.
Original solution:
I've sorted this, for now, with help from David Walsh's post here. It's still ugly, but works for now.
Updated the onChanged Proxy maker with a recursive-ish get trap.
get: function (target, property, receiver) {
let retval;
try {
retval = new Proxy(target[property], handler);
} catch (err) {
retval = Reflect.get(target, property, receiver);
}
if (mutators.includes(property))
onChange(target, property, receiver);
return retval;
},
Also added a list of functions to check the get trap against (the ugly, hacky bit):
const mutators = [
"push",
"pop",
"shift",
"unshift",
"splice",
"reverse",
"fill",
"sort"
]
This seems to be working in my testing so far.
Thanks for pointing in the correct direction.

How to Know if a Proxy call/access is nested?

I'm working with JS Proxies for fun and have made decent progress. But currently it's all at a single level. What I would like is to have nested Proxies be returned if I'm making a nested call/access, otherwise just return the object.
// example calls/accesses
data.settings = {fire: true};
data.settings.fire // nested call
// returns true
data.settings // top level call
// returns {fire: true}
// the proxy code
const data = new Proxy({}, {
get: function(target, property, receiver) {
// how to figure out nestedCall?
if (nestedCall) {
return new Proxy(target[property], {
get: function(subTarget, subProperty, subReceiver) {
return 'nonsense, there is nothing nested here';
}.
});
}
else {
return target[property];
}
},
});
Is this even possible?
Is this even possible?
No, it is not possible to distinguish
const val = data.settings.fire; // two accesses
from
const obj = data.settings; // one access
const val = obj.fire; // another access
and return a plain object, instead of a proxy for it, for .settings only in the second case.
Just use a 'set' trap. This 'set' trap example proxies objects as they are being assigned. You can alter the criteria to be more sophisticated as needed.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#A_complete_traps_list_example
const whatICareAbout = ['settings', 'mediaSettings', 'fire', 'video'];
const proxy = {
get: function(obj, prop) {
if(whatICareAbout.includes(prop)) console.log('Get...', prop);
return obj[prop];
},
set: function(obj, prop, value) {
if(typeof value === 'object') {
console.log('Proxy...', prop);
obj[prop] = new Proxy(value, proxy);
} else {
obj[prop] = value;
}
}
};
const p = new Proxy({}, proxy);
p.settings = {fire: true};
p.settings.mediaSettings = {video: false};
console.log(p.settings);
console.log(p.settings.mediaSettings);

Creating jQuery prototyped collection

I need to initialize some kind of a prototype on already existing jQuery elements collection. The main problem is that the prototype should be accessible only inside of that collection and on elements produced by built-in jQuery functions like .find() on that collection or on some children objects inside of that collection, for example:
var $a = $('a');
$a.__proto__.foo/*some magic over here*/ = function(){ alert('foo!'); };
$a.foo(); //should show alert('foo!')
$a.find('b').foo(); //should produce the same action
$('a').foo(); //should produce an error (method not found)
If using $a.__proto__ like in example above, the jQuery.prototype is accessed, so all the new elements in outside of that jQuery-collection (for example, $('a')) are granting an access to .foo() method. That behaviour is unacceptable on a problem statement.
Is that actually possible?
Okay, here's the thing, I have a rather complex ES6 solution, so I won't be able to explain it in great depth, but if you have some particular questions, go ahead.
var wrap = (function wrapper() {
var store = {};
function wrap(fn) {
return new Proxy(fn, {
apply(target, thisArg, argumentsList) {
var result = Reflect.apply(target, thisArg, argumentsList);
// `jQuery(...)` returns a "rich" object that always contain `.length`
if (result.length > 0) {
result = new Proxy(result, {
get(target, propertyKey, receiver) {
var value = Reflect.get(target, propertyKey, receiver);
if (Object.keys(store).includes(propertyKey)) {
value = store[propertyKey];
}
return value;
},
set(target, propertyKey, value, receiver) {
// TODO: use `Reflect.set(), somehow`
// return Reflect.set(store, propertyKey, value, receiver);
return (store[propertyKey] = value);
},
});
}
return result;
}
});
}
return wrap;
})();
var $ = wrap(jQuery);
$.prototype.find = wrap(jQuery.prototype.find); // TODO: implement recursively in `wrap()`
var x = $('div');
var xx = x.find('div');
var xxx = x.find('divvv');
xx.foo = 123;
console.log(x.foo); // 123
console.log(xx.foo); // 123
console.log(xxx.foo); // undefined

Is it possible to change a Proxy's target?

I have a class that implements the XMLHttpRequest interface. Depending on the URL passed to open(), I can determine whether to use the default XMLHttpRequest or my custom implementation. My idea is to use a proxy to do this:
let xhr = new XHRProxy();
xhr.open('GET', 'http://blah'); // Decide here depending on URL
I did some tests using the ES6 Proxy, which seems promising, but unfortunately the proxy target cannot be modified after constructing the Proxy:
var foo = {
name() {
return "foo";
}
};
var bar = {
name() {
return "bar";
}
}
var handler = {
get(target, property, receiver) {
if (property === "switchToBar") {
// FIXME: This doesn't work because a Proxy's target is not exposed AFAIK
receiver.target = bar;
return function() {};
} else {
return target[property];
}
}
}
var proxy = new Proxy(foo, handler);
console.log(proxy.name()); // foo
proxy.switchToBar();
console.log(proxy.name()); // foo :(
I think I can accomplish what I want by not setting a target at all - instead defining all traps to delegate to the desired object - but I'm hoping for a simpler solution.
Here's a go at "defining all traps to delegate to desired object"
(function () {
let mutableTarget;
let mutableHandler;
function setTarget(target) {
if (!(target instanceof Object)) {
throw new Error(`Target "${target}" is not an object`);
}
mutableTarget = target;
}
function setHandler(handler) {
Object.keys(handler).forEach(key => {
const value = handler[key];
if (typeof value !== 'function') {
throw new Error(`Trap "${key}: ${value}" is not a function`);
}
if (!Reflect[key]) {
throw new Error(`Trap "${key}: ${value}" is not a valid trap`);
}
});
mutableHandler = handler;
}
function mutableProxyFactory() {
setTarget(() => {});
setHandler(Reflect);
// Dynamically forward all the traps to the associated methods on the mutable handler
const handler = new Proxy({}, {
get(target, property) {
return (...args) => mutableHandler[property].apply(null, [mutableTarget, ...args.slice(1)]);
}
});
return {
setTarget,
setHandler,
getTarget() {
return mutableTarget;
},
getHandler() {
return mutableHandler;
},
proxy: new Proxy(mutableTarget, handler)
};
}
window.mutableProxyFactory = mutableProxyFactory;
})();
const {
proxy,
setTarget
} = mutableProxyFactory();
setTarget(() => 0);
console.log(`returns: ${proxy()}`);
setTarget({ val: 1 });
console.log(`val is: ${proxy.val}`);
setTarget({ val: 2 });
console.log(`val is: ${proxy.val}`);
setTarget(() => 3);
console.log(`returns: ${proxy()}`);
I feel like there must be some reason this isn't supported out of the box, but I don't have enough information to comment on that further.
After hacking on this for a while, I've observed a few things. It seems the original target that the proxy constructor is called with is treated as part of the proxy's identity regardless. Setting the original target to a plain object and swapping the target to a function later raises an error, when the proxy is called. There are definitely some rough edges to this, so use with caution.
I took John's answer and improved it (I hope):
const mutableProxyFactory = (mutableTarget, mutableHandler = Reflect) => ({
setTarget(target) {
new Proxy(target, {}); // test target validity
mutableTarget = target;
},
setHandler(handler) {
new Proxy({}, handler); // test handler validity
Object.keys(handler).forEach(key => {
const value = handler[key];
if (Reflect[key] && typeof value !== 'function') {
throw new Error(`Trap "${key}: ${value}" is not a function`);
}
});
mutableHandler = handler;
},
getTarget() {
return mutableTarget;
},
getHandler() {
return mutableHandler;
},
proxy: new Proxy(
mutableTarget,
new Proxy({}, {
// Dynamically forward all the traps to the associated methods on the mutable handler
get(target, property) {
return (_target, ...args) => mutableHandler[property].apply(mutableHandler, [mutableTarget, ...args]);
}
}),
)
});
Some important differences:
John's version only has one mutableTarget/Handler variable shared by all results from mutableProxyFactory, so you probably can't call it multiple times.
The handler is allowed to have additional keys, because there's no reason it shouldn't.
Traps in the handler have this bound to the handler, as in a normal Proxy.
The user can give initial values for the handler and target. A value for the target is actually required.
mutableProxyFactory isn't assigned globally to window.
EDIT: here's a similar version but implemented as a TypeScript class. Haven't tested this one.
class MutableProxy<T extends object> {
constructor(private _target: T, private _handler: ProxyHandler<T>) {
this.target = _target;
this.handler = _handler;
}
public get target() {
return this._target;
}
public set target(target: T) {
new Proxy(target, {}); // test target validity
this._target = target;
}
public get handler() {
return this._handler;
}
public set handler(handler: ProxyHandler<T>) {
new Proxy({}, handler); // test handler validity
for (const [key, value] of Object.entries(handler)) {
if (Reflect.has(Reflect, key) && typeof value !== 'function') {
throw new Error(`Trap "${key}: ${value}" is not a function`);
}
}
this._handler = handler;
}
public get proxy() {
return new Proxy(
this.target,
new Proxy({}, {
// Dynamically forward all the traps to the associated methods on the mutable handler
get(target, property) {
return (_target: T, ...args: any[]) => {
const {handler} = this;
return handler[property].apply(handler, [this.target, ...args]);
};
}
}),
);
}
}
Is it possible to change a Proxy's target?
No, this is not possible. The proxy handler is a quite generic interface already, and by defining all traps to forward the operation to a different handler this is easily achievable. That's why there is no extra method to change the target, the interface is kept minimal. By not making the target changeable, also the shape of the proxy is preserved (e.g. whether it's callable or an array).
How about this? Instead of making foo or bar the target directly, we use a box for our target, and put foo or bar into the box.
var foo = {
name() {
return "foo";
}
};
var bar = {
name() {
return "bar";
}
};
var handler = {
get(target, property, receiver) {
if (property === "switchToBar") {
target.content = bar;
return function() {};
} else {
return target.content[property];
}
}
};
var box = {content: foo};
var proxy = new Proxy(box, handler);
console.log(proxy.name()); // foo
// Switch over to bar by calling the function
proxy.switchToBar();
// Or, we could do the switch from out here
box.content = bar;
// Either way, we get the same result
console.log(proxy.name()); // bar
In this case, our box is an object with property content. But alternatively, you could use an array with an item at index 0.

Defining read-only properties in JavaScript

Given an object obj, I would like to define a read-only property 'prop' and set its value to val. Is this the proper way to do that?
Object.defineProperty( obj, 'prop', {
get: function () {
return val;
}
});
The result should be (for val = 'test'):
obj.prop; // 'test'
obj.prop = 'changed';
obj.prop; // still 'test' since it's read-only
This method works btw: http://jsfiddle.net/GHMjN/
I'm just unsure if this is the easiest / smoothest / most proper way to do it...
You could instead use the writable property of the property descriptor, which prevents the need for a get accessor:
var obj = {};
Object.defineProperty(obj, "prop", {
value: "test",
writable: false
});
As mentioned in the comments, the writable option defaults to false so you can omit it in this case:
Object.defineProperty(obj, "prop", {
value: "test"
});
This is ECMAScript 5 so won't work in older browsers.
In new browsers or node.js it is possible to use Proxy to create read-only object.
var obj = {
prop: 'test'
}
obj = new Proxy(obj ,{
setProperty: function(target, key, value){
if(target.hasOwnProperty(key))
return target[key];
return target[key] = value;
},
get: function(target, key){
return target[key];
},
set: function(target, key, value){
return this.setProperty(target, key, value);
},
defineProperty: function (target, key, desc) {
return this.setProperty(target, key, desc.value);
},
deleteProperty: function(target, key) {
return false;
}
});
You can still assign new properties to that object, and they would be read-only as well.
Example
obj.prop
// > 'test'
obj.prop = 'changed';
obj.prop
// > 'test'
// New value
obj.myValue = 'foo';
obj.myValue = 'bar';
obj.myValue
// > 'foo'
In my case I needed an object where we can set its properties only once.
So I made it throw an error when somebody tries to change already set value.
class SetOnlyOnce {
#innerObj = {}; // private field, not accessible from outside
getCurrentPropertyName(){
const stack = new Error().stack; // probably not really performant method
const name = stack.match(/\[as (\w+)\]/)[1];
return name;
}
getValue(){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] === undefined){
throw new Error('No global param value set for property: ' + key);
}
return this.#innerObj[key];
}
setValue(value){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] !== undefined){
throw new Error('Changing global parameters is prohibited, as it easily leads to errors: ' + key)
}
this.#innerObj[key] = value;
}
}
class GlobalParams extends SetOnlyOnce {
get couchbaseBucket() { return this.getValue()}
set couchbaseBucket(value){ this.setValue(value)}
get elasticIndex() { return this.getValue()}
set elasticIndex(value){ this.setValue(value)}
}
const _globalParams = new GlobalParams();
_globalParams.couchbaseBucket = 'some-bucket';
_globalParams.elasticIndex = 'some-index';
console.log(_globalParams.couchbaseBucket)
console.log(_globalParams.elasticIndex)
_globalParams.elasticIndex = 'another-index'; // ERROR is thrown here
console.log(_globalParams.elasticIndex)
Because of the old browsers (backwards compatibility) I had to come up with accessor functions for properties. I made it part of bob.js:
var obj = { };
//declare read-only property.
bob.prop.namedProp(obj, 'name', 'Bob', true);
//declare read-write property.
bob.prop.namedProp(obj, 'age', 1);
//get values of properties.
console.log(bob.string.formatString('{0} is {1} years old.', obj.get_name(), obj.get_age()));
//set value of read-write property.
obj.set_age(2);
console.log(bob.string.formatString('Now {0} is {1} years old.', obj.get_name(), obj.get_age()));
//cannot set read-only property of obj. Next line would throw an error.
// obj.set_name('Rob');
//Output:
//========
// Bob is 1 years old.
// Now Bob is 2 years old.
I hope it helps.
I tried and it Works ...
element.readOnly = "readOnly" (then .readonly-> true)
element.readOnly = "" (then .readonly-> false)

Categories