How to define deleter similar to getter and setter - javascript

I have a code that does get, set for nested objects using proxy. I want to handle delete also. I'm not sure how to do it.
In the below code, I use proxy to define getter and setter, but I also want to tap whenever a property gets deleted.
/*
This function takes an object and converts to a proxy object.
It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {
return new Proxy(original, {
get(target, name, receiver) {
let rv = Reflect.get(target, name, receiver);
return rv;
},
set(target, name, value, receiver) {
// Proxies new objects
if(typeof value === "object"){
value = getProxy(value);
}
return Reflect.set(target, name, value, receiver);
}
})
}
let first = {};
let proxy = getProxy(first);
/*
Here are the tests
*/
proxy.name={} // object
proxy.name.first={} // nested object
proxy.name.first.names=[] // nested array
proxy.name.first.names[0]={first:"vetri"} // nested array with an object
/*
Here are the serialised values
*/
console.log(JSON.stringify(first)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}

Figured out that deleteProperty handler takes care of it
/*
This function takes an object and converts to a proxy object.
It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {
return new Proxy(original, {
get(target, name, receiver) {
let rv = Reflect.get(target, name, receiver);
return rv;
},
set(target, name, value, receiver) {
// Proxies new objects
if(typeof value === "object"){
value = getProxy(value);
}
return Reflect.set(target, name, value, receiver);
},
deleteProperty: function(target, property) {
if (property in target) {
delete target[property];
}
}
})
}
let first = {};
let proxy = getProxy(first);
/*
Here are the tests
*/
proxy.name={} // object
proxy.name.first={} // nested object
proxy.name.first.names=[] // nested array
proxy.name.first.names[0]={first:"vetri"} // nested array with an object
/*
Here are the serialised values
*/
console.log(JSON.stringify(first)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
delete proxy.name.first.names[0].first // call goes through the delete handler

Related

JavaScript Proxy Setter doesn't make a second Proxy call

I have the following code that is using Proxy for Class setter. In my example I am tracking specific variable to update some other variables. My Setter is writing a log of all changes to Console. However if I try to modify a variable from a setter itself, variable gas modified, but the Proxy isn't called. Is it by design to avoid looping? Or am I missing something?
class darthVader {
constructor() {
return new Proxy(this, {
set(obj, prop, value) {
console.log(`Setting ${prop} to ${value}`)
obj[prop] = value
return true
}
})
}
set resistance(val) {
this._resistance= val
this.darkSide = false
}
get resistance() { return this._R2D2 }
}
let newHero = new darthVader()
newHero.resistance = 11
console.log(newHero.darkSide)
The problem is that your trap just runs obj[prop] = value, which sets a property on the target obj not on the proxy. What you should do instead is to use the Reflect.set method that provides the default implementation for the set trap, and expects an optional receiver argument. This receiver is the object that setters will be evaluated against, and you should pass the receiver argument of the set trap (which will refer to the newHero proxy that you assigned resistance to).
class DarthVader {
set resistance(val) {
this._resistance= val
this.darkSide = false
}
get resistance() { return this._R2D2 }
}
let newHero = new Proxy(new DarthVader, {
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`)
return Reflect.set(target, prop, value, receiver)
// ^^^^^^^^^^^
// obj[prop] = value
}
});
newHero.resistance = 11
console.log(newHero.darkSide)
The obj inside the set method refers to what this is when you do return new Proxy(this, and that object is not a proxy, but the darthVader instance itself - the one that's in the process of being created by the darthVader constructor. So, when you assign to a property of obj, you're putting a property directly on the darthVader instance, rather than on the proxy instance (which is the newHero). So, the proxy method doesn't get called.
If you wanted to recursively invoke the proxy, you could define it (let's say, as the variable name proxy) before returning it from the constructor, and then reference proxy inside the set method, but given the current logic, this results in a stack overflow because you'd be continually calling the proxy's setter:
class darthVader {
constructor() {
const proxy = new Proxy(this, {
set(obj, prop, value) {
console.log(`Setting ${prop} to ${value}`)
proxy[prop] = value
return true
}
})
return proxy;
}
set resistance(val) {
this._resistance = val
this.darkSide = false
}
get resistance() {
return this._R2D2
}
}
let newHero = new darthVader()
newHero.resistance = 11
console.log(newHero.darkSide)

Property accessors vs. Map set method

Is there a way for the following code to either
raise an error on m[k] = v or
make the call m[k] = v automatically translate to m.set(k, v)
I would prefer solution 1. if possible.
// Imagine I write that somewhere
const m = new Map();
m.set("a", "alice");
// And at some point someone else write:
m["b"] = "bob"; // m = Map { 'a' => 'alice', b: 'bob' }
// Expecting "bob" to appear here:
for (const [k, v] of m){
console.log(k, v);
}
Note that when I say someone else, this other person could simply be me in the near future. Ideally I would like to have a solution that only modifies the instantiation const m = new Map(). For example to something like const m = safe(new Map()).
To just prevent properties being added you can use:
Object.freeze(map);
However, that won't throw an error. To be able to throw an error on a property access you have to use a Proxy, however as that does not work on Maps directly (as they got internal properties that do not get reflected through the Proxy) you'd have to mirror all methods in an object, and use the Proxy on that:
function safe(map) {
return new Proxy({
get(k) { return map.get(k); },
set(k, v) { return map.set(k, v); }
// .... others
}, {
set() { throw new Error("property setter on Map"); }
});
}
You could also trap the Proxies getter to directly link to the Map (not sure about side effects though):
function safe(map) {
return new Proxy({ }, {
set() { throw new Error("property setter on Map"); },
get(target, prop, receiver) {
return typeof map[prop] === "function" ? map[prop].bind(map) : map[prop];
}
});
}
you can use proxy
let m = new Map();
m["a"] = "alice";
m = new Proxy(m, {
set(target, name, receiver) { throw "Setting values is forbidden!"; }
});
console.log(m["a"]); // you can read value
m["b"] = "bob"; // setting value will throw exception
You can use the Proxy object. Instantiate it with the first argument being an instantiation of whatever Map you want.
const m = new Proxy(new Map(), {
get(target, name, receiver) {
return target.get(name);
},
set(target, name, receiver) {
// 1. Comment in to allow error to be thrown.
// throw new Error(`Cannot set property for ${name}`);
// 2. Comment in to allow setting via bracket syntax.
// target.set(name, receiver);
}
});

Transform a Javascript object into a proxy (and not its reference)

I can take a Javascript object o and create a new Proxy object from it:
let p = new Proxy(object, { ... })
But is there a way to mutate an existing object reference to track changes on the original object? In particular, is there a way I can track the addition of new keys on the object from exterior sources?
The Proxy spec supports defining a proxy on the prototype of an object as a means for inspecting actions on that object when they do not exist on the instance. While this isn't full parity with .watch() it does allow for your mentioned use case of knowing when new properties are added. Here is an example, with comments about caveats...
// assuming some existing property you didn't create...
const t = { existing: true };
// proxy a new prototype for that object...
const ctr = {};
Object.setPrototypeOf(t, new Proxy(ctr, {
get(target, key) {
console.log('icu get');
return Reflect.get(target, key) || ctr[key];
},
set(target, key, val) {
console.log('icu set');
// setting this container object instead of t keeps t clean,
// and allows get access to that property to continue being
// intercepted by the proxy
Reflect.set(ctr, key, val);
return true;
},
deleteProperty(target, key) {
console.log('icu delete');
delete ctr[key];
return true;
}
}));
// existing properties don't work
console.log('existing');
t.existing; // <nothing>
t.existing = false; // <nothing>
// new properties work
console.log('new');
t.test; // icu get
t.test = 4; // icu set
console.log(t.test); // icu get
// 4
// but this doesn't work (and I think it should be a bug)
console.log('delete');
delete t.test; // icu get
// <missing icu delete>
console.log(t.test); // 4
Just create the object first and keep a reference to it before creating its Proxy.
Now you can modify either of them (the original object or its Proxy) and the other will also receive the changes unless you prevent them on the Proxy:
const o = {};
const p = new Proxy(o, {
set: function(obj, prop, value) {
if (prop === 'd') {
return false;
}
obj[prop] = value;
return true;
},
});
// These operations are forwarded to the target object o:
p.a = 0;
p.b = 1;
// This one is prevented by the Proxy:
p.d = true;
// Both will have two properties, a and b:
console.log(o);
// You can also mutate the original object o and the Proxy will also get those changes:
o.c = false;
// Note that now the Proxy setter is not called, so you can do:
o.d = true;
// But the Proxy still gets the change:
console.log(p);
If you want to be notified when a new property is added, deleted or modified on an object without the possiblity that the original reference is used to mutate the original object directly, the only option you have is to create that object directly as a Proxy or overwrite the original one:
// Created from an empty object without a reference to it:
// const p = new Proxy({}, { ... });
// Overwrite the original reference:
let myObject = { a: 1, b: 2 };
myObject = new Proxy(myObject, {
set: function(obj, prop, value) {
if (prop in obj) {
console.log(`Property ${ prop } updated: ${ value }`);
} else {
console.log(`Property ${ prop } created: ${ value }`);
}
obj[prop] = value;
return true;
},
deleteProperty(obj, prop) {
console.log(`Property ${ prop } deleted`);
delete obj[prop];
}
});
// Now there's no way to access the original object we
// passed in as the Proxy's target!
myObject.a = true;
myObject.a = false;
delete myObject.a;
There used to be an Object.prototype.watch(), but it has been deprecated.

Call an objects setter when its children are assigned to

Let there be an object userSingleton defined as such:
var userSingleton = new function() {
var _user = undefined;
Object.defineProperty(this, 'activeUser', {
get: function() {
console.log("Getter called, done something cool");
return _user;
},
set: function(val) {
console.log("Setter called, do something cooler");
_user = val;
}
});
}
Now if I go to use it, userSingleton.activeUser = {name: 'John Doe'}; works great! I get a "Setter called, do something cooler".
However, if I try to do userSingleton.activeUser.name = 'John Doe'; I instead get a "Getter called, done something cool" and userSingleton._user is not updated.
What's happening is it's trying to set the name property of the object returned by the getter (userSingleton.activeUser).
How do I make it call a particular function when any (unknown at definition time) property is assigned to / modified?
A revisited solution Proxy based with a deeply nested example (NB: ECMA Script 2015):
var handler = {
get: function (target, key) {
return target[key];
},
set: function (target, key, value) {
do_action(target, key, value);
if (typeof value === 'object') {
target[key] = new Proxy(value, handler);
} else {
target[key] = value;
}
}
};
function do_action(target, key, value) {
console.log("firing action on:", key, value)
}
function singletonUser() {
if (!this._singleton) {
const _user = {}
this._singleton = {
activeUser: new Proxy(_user, handler)
};
}
return this._singleton;
}
var userSingleton = singletonUser();
userSingleton.activeUser.name = 'pippo';
userSingleton.activeUser.age = 10;
// a deeply nested example
userSingleton.activeUser.github = {};
userSingleton.activeUser.github.followers = ["gino", "pino"]

Referring to a proxy'd this inside a class getter

I have the following contrived code:
class Animal {
get age() {
return this.baseage + 10;
}
age2() {
return this.baseage + 10;
}
}
const handler = {
"get": function(target, key) {
if (key === "baseage") {
return 20;
}
return target[key];
}
};
const animal = new Proxy(new Animal(), handler);
console.log(animal.age);
console.log(animal.age2());
Which produces
NaN
30
On node 6.11.0.
I would expect the code in the class getter, specifically this.baseage, to go through the proxy's handler too, but that does not seem to be the case. Is there any reason for it?
return target[key]; is not the same behavior as the default get handler. This is the cause of the broken get age function.
const handler = {
"get": function(target, key) {
if (key === "baseage") {
return 20;
}
return target[key];
}
};
should be
const handler = {
"get": function(target, key, receiver) {
if (key === "baseage") {
return 20;
}
return Reflect.get(target, key, receiver);
}
};
When you do target[key] you are calling the get age(){, but you are calling it with target as this, which is the new Animal object, not the proxy. Since the Proxy object is the one that handles baseage, not the Animal, you get back undefined.
In this example, receiver is the actual proxy object, so you could potentially do receiver[key] to have your snippet work, but there are tons more edge cases that you'd still not be handling in a general way.
Every single Proxy handler function has a Reflect.XX version that exposes the default behavior. Whenever you're writing a proxy and just want it to act like it normally would, you should be using Reflect.

Categories

Resources