Marius Mucenicu
Marius Mucenicu

Reputation: 1783

Why does the Proxy object reflect changes beyond the target object?

I wanted to experiment a bit with the Proxy object, and got some unexpected results, as follows:

Test script

function Person(first, last, age) {
  this.first = first;
  this.last = last;
  this.age = age;
}

Person.prototype.greeting = function () {
  return `Hello my name is ${this.first} and I am ${this.age} years old`;
};

So in tracking how the prototype object is being modified, I added this wrapper:

let validator = {
    set: function(target, key, value) {
        console.log(`The property ${key} has been updated with ${value}`);
        target[key] = value;
        return true;
    }
};

Person.prototype = new Proxy(Person.prototype, validator);

let george = new Person('George', 'Clooney', 55);

Person.prototype.farewell = function () {
  return `Hello my name is ${this.first} and I will see you later`;
};

What I expected

The property: "farewell" has been updated with: "function () {
  return `Hello my name is ${this.first} and I will see you later`;
}"

and nothing else.

And of course, each time I added or removed something from the prototype, i.e Person.prototype or instance.constructor.prototype I expected to see the console.log() message.

However I did not expect to see anything when setting something on the instance, like:

george.someProp = 'another value'; // did NOT expect to see the console.log()


Output

The property: "first" has been updated with: "george"
The property: "last" has been updated with: "clooney"
The property: "age" has been updated with: "55"
The property: "farewell" has been updated with: "function () {
  return `Hello my name is ${this.first} and I will see you later`;
}"
Person.prototype
Proxy {greeting: ƒ, first: "George", last: "Clooney", age: 55, farewell: ƒ, constructor: ƒ}

It set all the properties on the prototype and nothing on the instance, and each time I set something on the instance it sets it straight on the prototype.

Evidently this is not the default behaviour, as if I remove that Proxy, every property set with this will be set on the instance itself and the prototype will start up empty (or in our case with just the greeting function).

What am I missing here ? A point in the right direction would be appreciated.

Upvotes: 7

Views: 1335

Answers (1)

Christian C. Salvadó
Christian C. Salvadó

Reputation: 827496

What you are missing is the fact that when you have a Proxy object in the prototype chain, the set handler will be called when you modify the child object.

In your example, when you set a property on the new instance, the set trap will be executed, the target will be the wrapped Person.prototype object, but there's a fourth argument, the receiver. This argument points to the object that the property has been accessed on.

To properly do the property assignment, you can use the Reflect.set API to set it:

Reflect.set(target, key, value, receiver);

That's why the Reflect API matches the proxy traps arguments.

So, in your example, we could use the Reflect API and you will see that Person.prototype doesn't get "polluted".

function Person(first, last, age) {
  this.first = first;
  this.last = last;
  this.age = age;
}

Person.prototype.greeting = function () {
  return `Hello my name is ${this.first} and I am ${this.age} years old`;
};


const validator = {
    set: function(target, key, value, receiver) {
        console.log(`The property ${key} has been updated with ${value}`);
        Reflect.set(target, key, value, receiver)
        return true;
    }
};

Person.prototype = new Proxy(Person.prototype, validator);

const george = new Person('George', 'Clooney', 55);

Person.prototype.farewell = function () {
  return `Hello my name is ${this.first} and I will see you later`;
};

console.log(george.hasOwnProperty('first')); // true
console.log(Person.prototype.hasOwnProperty('first')); // false

Upvotes: 5

Related Questions