13

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"?

Philip Kamenarsky
  • 2,757
  • 2
  • 24
  • 30
  • 1
    [Take a look here](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Copying_accessors) – evolutionxbox Apr 03 '17 at 13:18
  • 1
    No. Obviously, because `{}` isn't a proxy. Please, explain your case further. Do you want to just clone a proxy? – Estus Flask Apr 03 '17 at 13:42
  • @estus I am keeping a property read log in the proxy of an immutable object. I want to keep the logging functionality whenever somebody creates a new immutable version from the proxied object. – Philip Kamenarsky Apr 03 '17 at 14:03
  • Who's that 'somebody'? You can probably extend Proxy and instantiate it with `new` instead of Object.assign, but this depends on the details. – Estus Flask Apr 03 '17 at 14:46
  • 1
    @estus Specifically, I want to augment the state argument to a [Redux-style reducer](http://redux.js.org/docs/recipes/UsingObjectSpreadOperator.html) (i.e. a function `st ->st`), so that I know which properties have been read inside that reducer, no matter how deeply nested and how many immutable updates were performed. EDIT: I could provide an Immutable.js-style interface, but I'd like to try natively first. – Philip Kamenarsky Apr 03 '17 at 14:57
  • I don't think that Proxy is a proper way to do this, there are not so many good use cases for it. And it certainly can't be used in this manner, so this is likely XY problem. I would suggest to re-ask the question considering all the details you have. – Estus Flask Apr 03 '17 at 15:18
  • I'm having @PhilipKamenarsky same issue. In my case, I'm using an npm package in my app, and at a certain point in my code, I'm 'receiving' the proxy as a function argument. What I need to do is to take this proxy, which is being received as the function's argument, add some properties to it and then forward it to another function. How can I create a new instance of Proxy, that includes an exact copy of the original Proxy, and add properties to it? I think the answer to this question automatically answers Philip's. – Arnie Jul 06 '18 at 12:55

1 Answers1

14

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

Adassko
  • 5,201
  • 20
  • 37