Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Object.assign and proxies

Having the following object:

let obj = { id: 0 };

and the following Proxy:

let objProxy = new Proxy(obj, {
  get: (target, name) => {
    if (name == "id")
      return "id from proxy";
}});

Is it possible to "retain" the Proxy after an Object.assign() (or an object spread operator, which afaik is just syntax sugar for Object.assign())?

let objProxyNew = Object.assign({}, objProxy); // i.e. {...objProxy};

So that objProxyNew.id returns "id from proxy"?

like image 655
Philip Kamenarsky Avatar asked Apr 03 '17 13:04

Philip Kamenarsky


1 Answers

Seems like I'm the third person with the exact same problem and this is the closest question on stackoverflow I've found but it has no real answers so I had to investigate it by myself.

Accidentally the behavior that Philip wants in his example is the default behavior so no change is necessary:

let obj = { id: 0 };
let objProxy = new Proxy(obj, {
  get: (target, name) => {
    if (name == "id")
      return "id from proxy";
}});
    
let objProxyNew = Object.assign({}, objProxy);

console.log(objProxyNew.id); // "id from proxy"

But this works only for simple proxies where the names of properties on the proxied object is the same as for the final object.

Implementing {...obj} for javascript proxy object

Let's take a little more complicated example, a proxy for "zip" operation (combining separate arrays of keys and values into a single object):

let objProxy = new Proxy({
    keys: ["a", "b", "c", "d"],
    values: [1, 3, 5, 7]
}, {
    get(target, name) {
        var index = target.keys.indexOf(name);
        return index >= 0 ? target.values[target.keys.indexOf(name)] : false
    }
});

console.log(objProxy.c); // 5   

console.log({...objProxy}); // {keys: undefined, values: undefined}

Now we got properties from the original object, but no values for them because the proxy returns nothing for "keys" and "values" properties.

As I found out, this is happening because we haven't defined trap for "ownKeys" and Object.getOwnPropertyNames(target) is called by default.

Extending proxy with:

    ownKeys(target) { return target.keys; }

makes it even worse because no properties are cloned now at all:

console.log({...objProxy}); // {}

What is happening right now is that Object.assign calls Object.getOwnPropertyDescriptor for every key returned by "ownKeys" function. By default property descriptors are retrieved from "target" but we can change it once again with another trap called "getOwnPropertyDescriptor":

let objProxy = new Proxy({
    keys: ["a", "b", "c", "d"],
    values: [1, 3, 5, 7]
}, {
    get(target, name) {
        var index = target.keys.indexOf(name);
        return index >= 0 ? target.values[index] : false
    },
    ownKeys(target) {
        return target.keys;
    },
    getOwnPropertyDescriptor(target, name) {
        return { value: this.get(target, name), configurable: true, enumerable: true };
    }
});

enumerable controls which properties will be cloned and visible in console. configurable must be set for proxied properties, otherwise we will get error:

VM1028:1 Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'a' which is either non-existent or configurable in the proxy target at :1:1

we also need to set "writable" to "true" to be able to set property in strict mode.

value seems to be not used by Object.assign but may be used by some other framework or implementation. If it's costly to actually get a value, we can define it as a getter:

    get value() { return this.get(target, name); }

To support in operator and to have consistent implementation, we should also implement "has" trap. So the final implementation can look like this:

let objProxy = new Proxy({
	keys: ["a", "b", "c", "d"],
	values: [1, 3, 5, 7]
}, {
	get(target, name) {
		var index = target.keys.indexOf(name);
		return index >= 0 ? target.values[index] : false
	},
	ownKeys: (target) => target.keys,
	getOwnPropertyDescriptor(target, name) {
		const proxy = this;
		return { get value() { return proxy.get(target, name); }, configurable: true, enumerable: true };
	},
	has: (target, name) => target.keys.indexOf(name) >= 0
});

console.log({...objProxy}); // {a: 1, b: 3, c: 5, d: 7}

Implementing [...obj] for javascript proxy object

Another story is to support [...objProxy] - here, [Symbol.iterator] is called which we need to define in a getter:

let objProxy = new Proxy({
	values: [1, 2, 3, 4],
	delta: [9, 8, 7, 6]
}, {
	get(target, name){
		if (name === Symbol.iterator) {
			return function*() {
				for (let i = 0; i < target.values.length; i ++) { yield target.values[i] + target.delta[i]; }
			}
		}
		return target.values[name] + target.delta[name];
	}
});

console.log([...objProxy]); // [10, 10, 10, 10]

we can also just proxy "Symbol.iterator" to original object:

return () => target.values[Symbol.iterator]();

or

return target.values[Symbol.iterator].bind(target.values);

we need to re-bind original context because otherwise iterator will be executed for Proxy object

like image 51
Adassko Avatar answered Oct 09 '22 19:10

Adassko