JavaScript Proxy Setter doesn't make a second Proxy call - javascript

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)

Related

Log what getter gets using proxy

I have the following proxy handler that logs get events.
const proxyHandler = {
get: function(target, prop) {
console.log("get", prop);
return Reflect.get(target, prop);
}
};
const obj = new Proxy(
{
value: 4,
text: "hi",
get textVal() {
return this.text.repeat(this.value);
},
getTextVal() {
return this.text.repeat(this.value);
}
},
proxyHandler
);
console.log("------- normal func -------")
console.log(obj.getTextVal());
console.log("------- getter func -------")
console.log(obj.textVal);
When I log console.log(obj.getTextVal()) I get:
get getTextVal
get text
get value
hihihihi
But when I log the getter console.log(obj.textVal), I only get the following:
get textVal
hihihihi
How can I make obj.textVal log the get text and get get value events using proxy? ie. When running console.log(obj.textVal) I would like the following result.
get getTextVal
get text
get value
hihihihi
The above answer works but there is a bit more elegant solution. You are missing the receiver in your Proxy trap and Reflect arguments. Simply change the Proxy to this:
const proxyHandler = {
get: function(target, prop, receiver) {
console.log("get", prop);
return Reflect.get(target, prop, receiver);
}
};
Notice the new receiver in the trap and Reflect arguments.
There is an important distinction between a Proxy trap target and receiver. In this case, the target is the underlying raw object while the receiver is the Proxy wrapper. If you do not pass the receiver to the Reflect call everything inside the get operation will be run against the raw object and won't trigger the Proxy traps.
If you have the time I suggest you read the relevant parts of the ES6 spec to fully grasp the difference between these two. Otherwise, just make sure that you forward all Proxy trap args to the matching Reflect call if you are aiming for a transparent wrap.
You can set the Proxy instance to proxyHandler object and access the properties through it (instead of this).
const proxyHandler = {
get: function(target, prop) {
console.log("get", prop);
return Reflect.get(target, prop);
}
};
const proxifiedObj = {
value: 4,
text: "hi",
get textVal() {
return this.proxyInstance.text.repeat(this.proxyInstance.value);
},
getTextVal() {
return this.text.repeat(this.value);
}
}
obj = proxifiedObj.proxyInstance = new Proxy(proxifiedObj, proxyHandler);
console.log("------- normal func -------")
console.log(obj.getTextVal());
console.log("------- getter func -------")
console.log(obj.textVal);
console.log(obj.textVal);
get getTextVal
get text
get value
hihihihi
Update:
Or you could do the same thing by creating a custom Proxy that does the assignment for you
(Note: Proxy class cannot be extended but we can use the constructor return value pattern):
class InstanceAwareProxy {
constructor(proxifiedObject, proxyHandler) {
return proxifiedObject.proxyInstance
= new Proxy(proxifiedObject, proxyHandler);
}
}
obj = new InstanceAwareProxy(proxifiedObj, proxyHandler);

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.

Typescript / Javascript custom Property decorators

I've just started to learn in more depth Typescript and ES6 capabilities and I have some misunderstandings regarding how you should create and how it actually works a custom Property Decorator.
This are the sources that I'm following
Source1
Source2
And this is my custom deocrator.
export const Required = (target: Object, key: string) => {
let value: any = target[key];
const getter = () => {
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty)
}
const setter = (val) => value = val;
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
}
It is applied like so
export classMyClass{
#Required
public Items: number[];
}
What I don't understand is why it works differently then what would you expect. Well it works but I don't know if it works accordingly to the let's call it "decorator philosophy",
Let me explain what I don't understand
Starting with the first line of code.
let value: any = target[key];
I would expect that the value would be initialized with Items value, but instead is undefined, why? how? i really don't understand.
I've followed both sources and that first thing that I find it confusing is the fact that one used target[key] to initialize the value while the other used this[key], shouldn't this refer to the Required actually.
What I also find confusing is this part
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
Firstly, why I need to delete this[key] ? from my understanding this should refer to the current object context in my this case Required, and when debugged it is as so.
Secondly, that Object.defineProperty will create a property with the name of the key on the target class in my case MyClass, but isn't this property already there?
Moving forward and setting the value using the setter, how does the setter parameter val know what data it should hold ? I mean where is it coming ?
Thanks all.
There's a lot of questions in there, but I'll try to answer them all :)
I would expect that the value would be initialized with Items value, but instead is undefined, why?
When your MyClass class is instantiated and your decorator code is run, the value of all properties of the object are undefined (even if you use a TypeScript property initializer, which you aren't).
Shouldn't this refer to the Required?
Yes, I think it does in your example. But in your Source1 link, the decorator is defined using a function expression (function logProperty()); in your example, you have switched this to an arrow function expression (const Required = ( ... ) =>). It's likely this switch will change what this refers to. To be safe, switch this to target[key].
Why I need to delete this[key]?
This block of code is deleting the original Items property and replacing it with a property of the same name that allows you to spy on the getting and setting of the variable. The delete this[key] line is guarding against the case where the property isn't configurable. (The delete operator returns false if the property in question is non-configurable:).
Object.defineProperty will create a property with the name of the key on the target class in my case MyClass, but isn't this property already there?
Yes, as mentioned above - this code is replacing the property with a new property. The new property is set up to allow you observe the getting and setting of this variable.
Moving forward and setting the value using the setter, how does the setter parameter val know what data it should hold ? I mean where is it coming ?
When your new Items property is set, the setter function is called with the soon-to-be new value of the property as a parameter (this setter function was previously set up as a setter function using Object.defineProperty). The implementation of this setter function sets value, which is a reference to target[key]. As a result, the value of Items gets updated.
To better understand how all this is working, try adding in some logging statements and then play around with getting/setting the Items property:
export const Required = (target: Object, key: string) => {
let value: any = target[key];
console.log('Initialize the Required decorator; value:', value);
const getter = () => {
console.log('Inside the getter; value is:', value);
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty);
};
const setter = val => {
console.log('Inside the setter; val is: ', val, 'value is:', value);
return (value = val);
};
if (delete this[key]) {
console.log('Replacing the "' + key + '" property with a new, configured version');
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
};
export class MyClass {
#Required
public Items: number[];
}
// instantiated your class
var mc = new MyClass();
// set the Items property
mc.Items = [4, 5, 6];
// get the value with no Errors
mc.Items;
// set the Items property to undefined
mc.Items = undefined;
// get the value - an Error is thrown
mc.Items;

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.

How to write a mock object that will never throw "Cannot read property '**' of undefined'" in JavaScript

With the intention to write unit tests, I want to pass an object to my function that mocks any possible property - Note: not a function property.
When I have a function like this:
function someFunc (config){
var something = config.params.innerParams;
}
when called someFunc({}) it will throw Cannot read property 'innerParams' of undefined'.
If there are many recursive properties in config, mocking it may be very time consuming. Is there a way to write a "mock" object that I can pass to my function to mimic any structure? It may assign undefined to all properties at the time of access, like this:
var magic = new Magic(); //magical mock created
var a1 = magic.something; //undefined;
var a2 = magic.something.innerSomething; //undefined
var a3 = magic.something.innerSomething.farAwaySomething; //undefined
All I want is to avoid Cannot read property '*' of undefined' being thrown.
You can use ES6 Proxy to pass access to notexisting property of object.
I would've return the object itself in that case.
But I think, it would be difficult to archive equallity to undefined.
At least I don't know the way (but I have some thoughts).
And don't forget to check Compatibility table.
function Magic() {
var res = new Proxy(this, { get(target, key, receiver) { return res } });
return res;
}
Can't make it equal to undefined, but can to false (nonstrictly):
function Magic() {
var res = new Proxy(this, { get(target, key, receiver) { return key === Symbol.toPrimitive ? Reflect.get(target, key, receiver) : res } });
res[Symbol.toPrimitive] = () => false;
return res;
}
Tested in FF44.
If you have Proxy support (Firefox, IE11, and Edge for now), you can pass in
new Proxy({}, { get(a,b,p) { return p; } })
This is a Proxy object which allows access on properties of any name. When accessed, the value of every such property is the original proxy object itself, thereby allowing infinite chains of property access.
Property access does not yield undefined, but there is no way to do that while simultaneously allowing further property access.

Categories

Resources