Javascript Proxy and spread syntax, combined with console.log
Asked Answered
C

2

10

So, I was playing around with Proxy objects and while trying to see how they mix with spread syntax and de-structuring, I stubled upon this weird behavior:

const obj = {
  origAttr: 'hi'
}

const handler = {
  get(target, prop) {
    console.log(prop);
    return 1;
  },
  has(target, prop) {
    return true;
  },
  ownKeys(target) {
    return [...Reflect.ownKeys(target), 'a', 'b'];
  },
  getOwnPropertyDescriptor(target, key) {
    return {
      enumerable: true,
      configurable: true
    };
  }
}

const test = new Proxy(obj, handler);
const testSpread = { ...test};

console.log('Iterate test');
// Works OK, output as expected
for (const i in test) {
  console.log(i, ' -> ', test[i]);
}

console.log('Iterate testSpread');
// Also works OK, output as expected
for (const i in testSpread) {
  console.log(i, ' -> ', testSpread[i]);
}

console.log('Here comes the unexpected output from console.log:');
console.log(test); // All attributes are 'undefined'
console.log(testSpread); // This is OK for some wierd reason

The above script outputs (on node v10.15.1):

Here comes the unexpected output from console log:

Symbol(nodejs.util.inspect.custom)
Symbol(Symbol.toStringTag)
Symbol(Symbol.iterator)
{ origAttr: undefined, a: undefined, b: undefined }
{ origAttr: 1, a: 1, b: 1 }

Why does console.log(test); output show that the attributes of the object are all undefined? This could cause some serious headache if it were to happen when debugging something.

Is it a bug in node itself or perhaps in the implementation of console.log?

Campstool answered 11/3, 2019 at 16:15 Comment(11)
This appears to be a bug; I get the expected output on the current version of Chrome.Dex
As do I on Safari (Mac) 12.0.3 (all good)Reproduction
Works fine on FF 65.0.1 on Mac as well.Reproduction
console.log's sourceFurnivall
Since I don't have access to test on node v10.15.1, could you add console.log(prop) inside of the get trap to see what properties it intercepts and how many times it does so?Dex
@JonasWilms I think this has less to do with console.log() itself and more to do with how a proxy object intercepts properties implicitly accessed via the object spread syntax on Node v10.15.1, at least that appears to be the source of the bug.Dex
@patrick I don't think so. If that would be the case testSpread would look weird. Or spreading does destroy the Proxy somehow.Furnivall
I added console.log(prop) to get. But it seems that this may be a bug in a specific version of nodejs, I'll try other versions as well later, then I'll know what to avoid in production when running code with proxies @Patrick RobertsProt
I'd recommend just avoiding proxies altogether in production. Any pattern that requires you to use proxies almost always has a code smell. They are unintuitive and it's not unheard of to see bugs like this resulting in inconsistent behavior across implementations. This is not the first bug report I've seen on Stack Overflow for proxy usage on v8.Dex
Found the source of the weird behavior and posted it as an answer @Patrick RobertsProt
This question is totally off-topic as OP's answer indicates the issue was clearly specifically caused by an error in his code, and is not a bug/nor anything overly re-producable outside the context of his question alone. Additionally, this question may have only been created solely so he could accept his own answer for rep.Bobwhite
C
4

Okay, I did some more digging and traced the whole thing down to Object.getOwnPropertyDescriptor being called on my proxy object to get the values of its attributes.

But the "value" attribute is obviously undefined in my case, since I have a trap for getOwnPropertyDescriptor which only specifies the enumerable and configurable attributes (thus making it possible to iterate the array, use it with spread operators and so on). As there is no standard way to invoke the get trap from the getOwnPropertyDescriptor trap, this can't really be fixed IMHO. Would be interesting to be proven wrong though :)

Well, as Bergi pointed out in the comments, there is a standard way.

Also in the docs https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor#Parameters "this is bound to the handler"

Edited my code to reflect that.

The code demonstrating getOwnPropertyDescriptor behavior is below:

const obj = {
  origAttr: 'hi'
}

const handler = {
  get(target, prop) {
    return 1;
  },
  has(target, prop) {
    return true;
  },
  ownKeys(target) {
    return [...Reflect.ownKeys(target), 'a', 'b'];
  },
  getOwnPropertyDescriptor(target, key) {
    return {
      value: this.get(target, key),
      enumerable: true,
      configurable: true
    };
  }
}

const test = new Proxy(obj, handler);
const testSpread = { ...test
};

// Defined, due to trapped getOwnPropertyDescriptor which returns a value attribute
console.log(Object.getOwnPropertyDescriptor(test, 'origAttr'))

// Defined, because it is a regular object, not a proxy with a getOwnPropertyDescriptor trap
console.log(Object.getOwnPropertyDescriptor(testSpread, 'origAttr'))
Campstool answered 12/3, 2019 at 10:43 Comment(3)
What do you mean by "As there is no standard way to invoke the get trap from the getOwnPropertyDescriptor trap"? Sounds like you are looking for Reflect. Or just this.get(target, key) maybe?Pademelon
@Pademelon You are right! I have been using javascript for quite some time, but asking and answering questions about JavaScript here for a short while has taught me a lot more about the language than I have been able to find out on my own. Thanks!Prot
this is the handler object, the trap handlers are called as methods.Pademelon
L
1

the Proxy object, as per definition Proxy is nothing but virtualisation of the object you are proxying with it.

Therefore the Proxy object itself has only the attributes of the object you are proxying, if you tried to run console.log(test) you will see the console will print out Proxy {origAttr: "hi"} but it will also have a handler and a target inside that you defined above.

When you instead use the spread operator you are creating a new object which it's created in the same way you would by iterating on the properties from your Proxy object like this:

Object.keys(test) --> ["origAttr", "a", "b"] because that's what you defined within ownKeys(target) { return [...Reflect.ownKeys(target), 'a', 'b']; }.

Then it will access test["origAttr"], then test["a"] and test["b"] using the proxy get function, which returns always 1.

As a result you object testSpread actually contains these attributes, while test does not.

And when you run console.log(testSpread) --> {origAttr: 1, a: 1, b: 1}

Lalittah answered 11/3, 2019 at 16:45 Comment(2)
If this explanation is true then why does everyone else see the expected behavior when trying the code in other versions or environments? The fact that there's an observable difference between implementations suggests that at least one of them has a bug, because the observable behavior of Proxies is well defined.Dex
Hi @PatrickRoberts thank for your feedback. I was actually reporting the behaviour of my Chrome console and testing a bit the way it worked locally. Happy they managed to understand what was the cause in the answer above.Lalittah

© 2022 - 2024 — McMap. All rights reserved.