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.
Related
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)
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
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)
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);
}
});
I'm trying to check if Object.assign is called on my object, and if that is falsy I do some further work on the object. How do I do this? My code looks like this:
if (Object.assign) {... }
Object.assign just copies properties over to an object. To find out if a property is being set, you can use a Proxy . You can't prevent Object.assign from working only on your object. You can either:
Override Object.assign so it doesn't set properties (bad idea)
Prevent everyone from setting some properties on your obect (maybe closer to what you want)
let validator = {
set: function(obj, prop, value) {
console.log('an attempt to set values has been made', obj, prop, value);
// The default behavior to store the value
obj[prop] = value;
return true;
}
};
let person = new Proxy({}, validator);
Object.assign(person, {a: 2});
I want to stop object.assign to being called on the object
Here's a hack I really don't recommend
var objectThatCannotBeObjectAssigned = {a:1};
var oldAssign = Object.assign;
Object.assign = function(target) {
if (target === objectThatCannotBeObjectAssigned) {
return target;
}
return oldAssign.apply(this, arguments);
}
console.log(Object.assign(objectThatCannotBeObjectAssigned, {a: 2}) );
console.log(Object.assign({a:1}, {a: 2}));