I'm trying to log items inserted into localStorage via the dot accessor using Chromium.
But i'm getting this error when using Reflect.get() method.
Uncaught TypeError: Illegal invocation
here's my script
const localStorageProxy = new Proxy(localStorage, {
get(target, name, receiver) {
// error here
const value = Reflect.get(target, name, receiver);
if (!['setItem', 'getItem', 'removeItem'].includes(name)) {
console.log(value);
console.trace();
}
return typeof value === 'function' ?
value.bind(target) :
value;
},
set(target, name, value, receiver) {
if (!['setItem', 'getItem', 'removeItem'].includes(name)) {
console.log(value);
console.trace();
}
if (name === 'setItem') {
return false;
}
// probably an error here too
return Reflect.set(target, name, value, receiver);
},
});
Object.defineProperty(window, 'localStorage', {
value: localStorageProxy,
configurable: true,
enumerable: true,
writable: false,
});
Expected result: window.localStorage.myItem = 'some-value' should log the value and a stack trace
This script is used with Tampermonkey on Chromium v74 (should be fine, supported by Chrome 49 and above).
How can I resolve this ?
Related
Question
How to create a proxy for browser native DOM object?
Background
I want to intercept the settings for the element style. So I create a proxy for the DOM object. However, it causes error when I use some function like getComputedStyle().
const setHandler = (target: any, prop: PropertyKey, value: any, _receiver?: any) => {
if (/*some condition*/) {
target[prop] = value
}
return true
}
const getHandler = (target: any, prop: PropertyKey, _receiver?: any) => {
return target[prop]
}
const style = new Proxy(el.style, {
get: getHandler,
set: setHandler
})
const classList = new Proxy(el.classList,{
get: getHandler,
set: setHandler
})
const proxy = new Proxy(el/*HTMLElement*/, {
get: (target, prop, _receiver) => {
if (prop === 'target') {
return target
}
if (prop === 'style') {
return style
}
if (prop === 'classList') {
return classList
}
return getHandler(target, prop, target)
},
set: setHandler
})
const style = getComputedStyle(el)
el is the native browser DOM object. In my code, there are many methods whose parameters are el, and these methods may modify el.
I want to prevent some of these methods from modifying el, so I am trying to proxy the el object.
But after the proxy, some methods for DOM objects can't be used on proxy objects (like getComputedStyle()).
Demo
I create a demo below.
(function() {
const node = document.querySelector('#demo')
const proxy = new Proxy(node, {
getPrototypeOf(target){
return Object.getPrototypeOf(target)
},
get(target, prop, receiver){
let value = target[prop]
if (typeof value === 'function') {
value = Function.prototype.bind.call(value, target)
}
return value
},
set(target, prop,value, receiver){
target[prop] = value
},
apply(target, args, newTarget) {
return Object.apply(target,args)
},
})
console.log(proxy)
console.log(Object.getPrototypeOf(proxy))
console.log(proxy.style)
// error: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
const style = getComputedStyle(proxy)
console.log(style)
})()
<div id='demo'>demo</div>
I need to create an object that stores another objects. Each property of the big object has two properties 'value' and 'callback'.
let bigObj = {
first: {
value: true,
callback: () => {}
},
second: {
value: false,
callback: () => {}
}, {...}
}
I want to be able to get and change the value property by using bigObj.first / bigObj.first = "false", and the callback.. through the classic method: bigObj.first.callback = () => {}.
Each time the property 'value' is changed, I want to call its callback function.
Here's what I did
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
key !== 'callback' && target[key].callback();
return true;
}
});
The problem is that I can not change the callback property.
proxy.first.callback = () => console.log('new cb'); // won't do anything.
Do you have any ideas on how I could change the code so it would work?
Thank you.
The way you have it set up, proxy.first is returning a boolean. So then proxy.first.callback = ends up being false.callback = or true.callback =. These at least don't throw exceptions, but they're useless. If the value was an object instead of a boolean, you could make the value itself be a proxy, but you can't create a proxy with a non-object as the target.
Another option would be to have a special value with which you set first, that tells it to insert the callback. Below is an example, where if you pass in an object like {callback: () => {}}, then it will insert that as the callback. But anything else it will get set as the value.
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
if (value && value.callback) {
target[key] ? target[key].callback = value.callback : target[key] = {value: null, callback: value.callback};
return true;
} else {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
target[key].callback();
return true;
}
}
});
proxy.first = {callback: () => console.log('got a callback')};
proxy.first = false;
Using this example: How to watch for array changes? i tried to adapt to watch for object chabges:
(function() {
if (!("Proxy" in window)) {
console.warn("Your browser doesn't support Proxies.");
return;
}
var ob = {"a":["a", "b", "c", "d"]};
// a proxy for our ob
var proxy = new Proxy(ob, {
apply: function(target, thisArg, argumentsList) {
return thisArg[target].apply(this, argumentList);
},
deleteProperty: function(target, property) {
console.log("Deleted %s", property);
return true;
},
set: function(target, property, value, receiver) {
target[property] = value;
console.log("Set %s to %o", property, value);
return true;
}
});
proxy.a.push("z");
console.log("Current state of array: %o", ob);
})();
But the push does not trigger the console.log in set. Only if i change:
var proxy = new Proxy(ob.a, {...
proxy.push("z");
Can anyone help me? Thank you
i'm trying to watch an objects value, and if it changes, so some stuff..
So this is my object,
var list = [{
object: undefined,
index: 0,
src: 'url here',
active: { val: 0 }
}]
So I made active an additional object as I create a new object from the above, but make the active value the value from above, this keeps a reference of this object between the 2 objects.
var newList = [];
newList.push({
object: undefined,
index: 0,
src: list[i].src,
active: list[i].active // Use reference to old list
});
So i'm trying to watch the active value like so:
(list.active).watch('val', function() {
if (list.active.val === 1) {
console.log('active');
list.object.classList.add('active');
} else {
console.log('not active');
list.object.classList.remove('active');
}
});
However it appears that when I watch this value it is being removed, as if I console.log out the list, then the value is set to undefined! I'm changing the value of list.active.val after adding the watch events.
Here is the Polyfill i'm using for the watch functionality.
// object.watch
if (!Object.prototype.watch) {
Object.defineProperty(Object.prototype, "watch", {
enumerable: false,
configurable: true,
writable: false,
value: function (prop, handler) {
var oldval = this[prop],
newval = oldval,
getter = function () {
return newval;
},
setter = function (val) {
oldval = newval;
return newval = handler.call(this, prop, oldval, val);
};
if (delete this[prop]) { // can't watch constants
Object.defineProperty(this, prop, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
});
}
// object.unwatch
if (!Object.prototype.unwatch) {
Object.defineProperty(Object.prototype, "unwatch", {
enumerable: false,
configurable: true,
writable: false,
value: function (prop) {
var val = this[prop];
delete this[prop]; // remove accessors
this[prop] = val;
}
});
}
Edit 1
Added the proxy-observe Polyfill however this doesn't appear to be watching still, I have added it like so:
list[0] = Object.observe(list[0], function(changeset) {
console.log('changed');
});
list[0].active contains the object { val: 0 }, so it should be observicing this object.
Not getting any errors, it's just doing nothing, ideas?
You don't need to implement a custom watch/unwatch features,
Ecmascript 2015 already provides a specific api:
Proxy
There are a plenty of polyfills to make it working on legacy browsers.
There was a proposal called Object.Observe to address what you need and you can find a Proxy porting here: https://github.com/anywhichway/proxy-observe
Follows a basic working example:
// Your Observable target
const target = Object.create(null);
const observer = {
set(target, key, value) {
console.log(`Should we set '${value}' as value of '${key}' property?`)
target[key] = value;
},
};
const observable = new Proxy(target, observer);
observable.someKindOfProperty = 'Hello World';
Fiddle
var Assertion = function() {
return { "dummy": "data" };
}
Object.defineProperty(Object.prototype, 'should', {
set: function(){},
get: function(){
return new Assertion(this);
}
});
// Insert magic here.
// This needs to be false
console.log(({}).should === undefined);
What options do I have in ES5 to undo a defineProperty call ?
No silly suggestions like Object.defineProperty = function() { } please.
The following Object.defineProperty(Object.prototype, 'should', {})
does not work
and Object.defineProperty(Object.prototype, 'should', { value: undefined })
Throws a Uncaught TypeError: Cannot redefine property: defineProperty in V8
Object.defineProperty(Object.prototype, 'should', {
set: function() {},
get: function() { return undefined; }
});
Throws the same error
delete Object.prototype.should also does not work
In general, you can't undo a defineProperty call, since there's no undo stack or something. The JS engine does not keep track of previous attribute descriptors.
For example,
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
value: 1,
enumerable: false
});
Object.defineProperty(Object.prototype, 'foo', {
get: function () {
alert('You cannot revert me');
return 2;
},
enumerable: true
});
What you can do is remove or reconfigure an attribute, or overwrite its value. As mentioned in the other answer, the configurable flag is required to be true if you want to remove or reconfigure.
Once a property is defined with configurable:false, you cannot change the configurable flag.
To remove an attribute (this is supposedly what you want to do), use delete:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true, // defaults to false
writable: false,
value: 1
});
delete Object.prototype.foo;
console.log(Object.prototype.hasOwnProperty('foo')); // false
To reconfigure, use defineProperty again and pass a different descriptor:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
get: ...
set: ...
});
Object.defineProperty(Object.prototype, 'foo', {
value: undefined
});
console.log({}.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true
As shown in this sample, you can use defineProperty to switch between accessor (get/set) and data (value) properties.
To overwrite, use simple assignment. In this case, you need the writable flag to be true. Obviously this does not work with accessor properties. It even throws an exception:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
value: 1,
writable: true // defaults to false
});
Object.prototype.foo = undefined;
console.log(Object.prototype.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true
Object.defineProperty(Object.prototype, 'foo', {
get: function () {
return 1;
},
writable: true // JS error!
});
Note that writable defaults to false when you use defineProperty, but true when you use the simple syntax o.attr = val; to define a (previously not existing) property.
If you want to undo your last defineProperty or all of them, you can use this class:
(gist here)
class PropertyDescriptorStack {
private readonly descriptors: PropertyDescriptor[] = [];
constructor(private readonly target: Object, private readonly prop: string) {
if (!target || typeof prop !== "string") { // your choice to define ""
throw new Error("PropertySaver: no object or property");
}
}
public push(props: Partial<PropertyDescriptor>): boolean {
this.saveDescriptor(this.target, this.prop);
try {
Object.defineProperty(this.target, this.prop, {
...props,
configurable: true,
});
return true;
}
catch (e) {
console.error(`Error setting property ${this.prop} on ${this.target}: ${e}`);
return false;
}
}
public pop(toStart?: boolean): boolean {
const ind = toStart ? 0 : this.descriptors.length - 1;
const descriptor = this.descriptors[ind];
if (!descriptor) {
return false;
}
this.descriptors.splice(ind, this.descriptors.length - ind);
try {
Object.defineProperty(this.target, this.prop, descriptor);
return true;
}
catch (e) {
console.error(`Error resetting property ${this.prop} on ${this.target}: ${e}`);
return false;
}
}
/**
* Saves the current descriptor of the property in the object in the descriptors stack.
* The descriptor is taken either from the object or from the closest prototype that has this prop.
* If none is found, a new descriptor is generated with the current value.
* #param target
* #param prop
* #returns The found descriptor
*/
private saveDescriptor(target: object, prop: string): PropertyDescriptor {
let ds: PropertyDescriptor | null = null;
for (let o: any = target, ds: PropertyDescriptor = null; o; o = Object.getPrototypeOf(o)) {
ds = Object.getOwnPropertyDescriptor(o, prop);
if (ds) {
break;
}
}
ds = ds || {
configurable: true,
writable: true,
value: target[prop],
enumerable: true
}
this.descriptors.push(ds);
return ds;
}
}