DEV Community

Cover image for JavaScript Jungle: Who is the Proxy
Vikas yadav for XenoX

Posted on

JavaScript Jungle: Who is the Proxy

I remember one of my interview few years ago. I was asked:

Given an object and how can we track when some property is accessed or updated on that object.

Example that was given by interviewer was as follows:

const obj = {name: 'Rick', planet: 'Earth'};

/**
* Case 1: Reading property:  
* Whenever I read any property
* it should print 
* GETTING value for <property_name> 
*/

obj.name; // GETTING value for name

/**
* Case 2: Setting property:  
* Whenever I set any property
* it should print 
* SETTING <property_name> = <new_value>
*/

 obj.planet = 'Mars' 
// SETTING planet = Mars 
Enter fullscreen mode Exit fullscreen mode

I was like what!!!

Confused

But any how I said to my self:

  • Let's pass this object into a function
  • Function somehow enhance this object and return it.

So I wrote:

function enhance(obj) {
  // do something with obj to enhance
}
Enter fullscreen mode Exit fullscreen mode

Interviewer asked me about my thought process on this? After thinking and remembering a lot πŸ€·πŸΌβ€β™‚οΈ I said, I know we have to some how intercept the read and write operation on the object, but I am not sure how to do it on the fly. I know when we define properties using Object.defineProperty, we have some control on how that property will behave but the object is already defined so I am not sure. Deep inside I was still not sure πŸ˜‚ I was still thinking πŸ™‡πŸ»β€β™‚οΈ:

Still thinking

Interviewer dropped me a hint that if I am using Object.defineProperty then I need to redefine the properties on the object. Then after some hit and trial I came up with this:

Tada

function enhance(plainObject) {
   const keys = Object.keys(plainObject);

  const storedObject = {};

  keys.forEach((key) => {
    storedObject[key] = plainObject[key];
    Object.defineProperty(plainObject, key, {
      get: function () {
        console.log(`GETTING value for  [${key}]`);
        return storedObject[key];
      },
      set: function (value) {
        console.log(`SETTING ${key} = ${value}`);
        storedObject[key] = value;
      }
    });
  });

  return plainObject;
}


let obj = { name: "rick", planet: "earth" };

const enhancedObj = enhance(obj);

console.clear();
console.log(enhancedObj.name);
// GETTING value for  [name] 
// rick 
enhancedObj.planet = "Mars";
// SETTING planet = Mars 

Enter fullscreen mode Exit fullscreen mode

🌴 Explanation:

  • As we know, we have to redefine the properties again. First thing we need to do is to stored all the existing keys in keys variable.
  • Defined a new object storedObject
  • Then for each key we copied everything form plainObject to storedObject
  • Next we defined all the properties on plainObject again but this time we used Object.defineProperty to defined it.
  • WE have overwritten the get which is called when we read property of an object and set which is called when we set a value to a property of an object.
  • We put the appropriate log there.
  • Also whenever set is called we will store that value in storedObject and return from storedObject when get is called.

During this time I was explaining my thought process to interviewer and I did a lot of hit and trial as it was hard for me to remember methods on Object.

My solution has issues some issues:

Still Thinking

  • If we add a new property on enhancedObject it will not be tracked. By the way, interviewer asked me this question as well πŸ˜†. But I could not come up with any solution back then. πŸ˜…

....

let obj = { 
    name: "rick", 
    planet: "earth" 
};

const enhancedObj = enhance(obj);

// this will not be tracked
enhancedObj.age = 30; 

Enter fullscreen mode Exit fullscreen mode

I was completely unaware that there is a better solution in ES6. After coming home when I researched, I found out a solution which is so much easy to implement as well as to understand.

Solution gif

Before we jump into our solution, let's learn a bit about JavaScript Specification of Object.


🌳 Internal Methods

JavaScript specification describes some lower level internal methods on Object like [[Get]], [[SET]], [[VALUE]], [[Writable]], [[Enumerable]] and [[Configurable]]. As per the specifications:

Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour.

Point to note: It defines runtime behaviour

But we cannot directly modify the behaviour of the Object at runtime using this so called Internal Methods as we cannot access it directly. As per the specifications:

These internal methods are not part of the ECMAScript language. They are defined by this specification purely for expository purposes.

There are some other internal methods as well. You can checkout full list here

But in ES6 we have a way to tap into these Internal methods at runtime.


🌳 Introducing Proxy

Proxy is a middleman. It does following:

  • It wraps another object.
  • It intercepts all the operations related to Internal Methods.
  • It can handle these operations by its own or it can forward these operation to the wrapped object.

Proxy Image


🌴 Anatomy of Proxy


let proxy = new Proxy(target, handler)

Enter fullscreen mode Exit fullscreen mode

Proxy is an inbuilt object that take two arguments:

  • target: An object that we need to wrap
  • handler: An object that defines various methods corresponding to Internal Methods that we need to intercept at run time.

Handler methods are often refer to as traps because it traps or intercept the Internal method.

Proxy relation

Example


const character = {
   name: 'Rick',
   planet: 'Earth'
};

const proxy = new Proxy(character, {});

// update name 

proxy.name = 'morty';


console.log(character.name) // morty

Enter fullscreen mode Exit fullscreen mode

πŸ‘Ύ GOTCHA πŸ‘Ύ : If you do not define any handler function. Proxy will pass the all the operations to wrapped object as if it is not there at all.

🌴 Handler methods

For each of the Internal Method there is a handler method defined on the Proxy object. Some of them are:

Internal Method Handler Method Triggered On
[[Get]] get When reading a property
[[Set]] set When writing a value to a property
[[HasProperty]] has When used with in operator
[[Delete]] deleteProperty When deleting a property with delete operator
[[Call]] apply When we do a function call

You can refer to full list on MDN Docs and TC39 docs

🌴 Invariants

There are certain condition attached to each of the handle methods. These condition must be fulfilled by the trap or handler methods. These are often referred as Invariants. You can read more on this in note section here.

As an example for [[SET]] Operation these are the invariants as per TC39 docs:

  • The result of [[Set]] is a Boolean value.
  • Cannot change the value of a property to be different from the value of the corresponding target object property if the corresponding target object property is a non-writable, non-configurable own data property.
  • Cannot set the value of a property if the corresponding target object property is a non-configurable own accessor property that has undefined as its [[Set]] attribute.

🌴 [[SET]] Trap

If we set a trap for [[SET]] operation and then we can modify the input before setting on original object name


const character = {
  name: "Rick",
  planet: "Earth"
};

const proxy = new Proxy(character, {
/**
* [[SET]] trap
* target: original object 
*         that has been wrapped
* prop: Property that has been set
* value: value to set to the property
*/
  set: function (target, prop, value) {
    // modifying the value 
    const capitalName = value.toUpperCase();
    // setting the value to target
    target[prop] = capitalName;
    return true;
  }
});

// update name

proxy.name = "morty";


// Log is MORTY instead of morty
console.log(character.name); // MORTY

Enter fullscreen mode Exit fullscreen mode

🌴 [[GET]] Trap

Same as [[SET]] trap we can set the [[GET]] trap. Suppose when we access a property we want to print the log Getting <property_name>. We can achieve that by using [[GET]] trap like this:


const character = {
  name: "Rick",
  planet: "Earth"
};

const proxy = new Proxy(character, {

/**
* [[GET]] trap
* target: original object 
*         that has been wrapped
* property: Property name 
*/

  get: function(target, property) {
    // printing the log before setting value
    console.log(`Getting: ${property} `);
    return target[property];
  }
});

const characterName = proxy.name;
console.log(characterName); 

// Getting: name  
// Rick 

Enter fullscreen mode Exit fullscreen mode

🌳 Reflect

Before I jump to Proxy solution of the problem. There is also a sister object of Proxy, which is known as Reflect. As per MDN docs

Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.

Point to note here is

  • It has methods same as Proxy
  • It is not a function
  • It is not constructible i.e you cannot use it like new Reflect

All the methods on Reflect are static so you can directly call them like

  • Reflect.get(...)
  • Reflect.set(...)

🌴 Relationship between Reflect and Proxy:

  • All the methods that you can define on the Proxy, Reflect has a same method with same argument.

  • Reflect can invoke the Internal Method by using the methods defined on it.

Proxy method Reflect call Internal method
get(target, property, receiver) Reflect.get(target, property, receiver) [[Get]]
set(target, property, value, receiver) Reflect.set(target, property, value, receiver) [[Set]]
delete(target, property) Reflect.deleteProperty(target, property)) [[Delete]]

You can check other methods of Reflect on MDN Reflect Docs

🌴 What do we need Reflect for

We know there are a lot of Invariants that we need to deal with when we trap some operation in Proxy and forward it to the original wrapped object. Remembering every rule can be hard.

We can simply use Reflect to forward any operation on original object and it will take care of all the Invariants

So now our [[SET]] and [[GET]] trap will change like this:

const character = {
  name: "Rick",
  planet: "Earth"
};

const proxy = new Proxy(character, {
  set: function (target, prop, value, receiver) {
    const capitalName = value.toUpperCase();
    return Reflect.set(target, prop, capitalName, receiver)
  },

  get: function(target, property, receiver) {
    console.log(`Getting: ${property} `);
    return Reflect.get(target, property, receiver);
  }
});

Enter fullscreen mode Exit fullscreen mode

🌳 Solution to original Problem:

With Proxy and Reflect now we can build our solution like this:

finally

 const proxyObject = function (obj) {
  return new Proxy(obj, {
    set: function (target, property, value, receiver) {
      console.log(`SETTING ${property} = ${value}`);
      return Reflect.set(target, property, value, receiver);
    },
    get: function (target, property, receiver) {
      console.log(`GETTING value for  [${property}]`);
      return Reflect.get(target, property, receiver);
    }
  });
};


let character = { name: "morty", planet: "earth" };

character = proxyObject(character);



console.log(character.name);
// GETTING value for  [name] 
// morty 
character.planet = "Mars";
// SETTING planet = Mars 


/**
* ES5 solution does not work if we set new properties
* ES6 Proxy will work for new properties as well and 
* print our log
*/

charter.grandParent = 'Rick';
// SETTING grandParent = Rick 

Enter fullscreen mode Exit fullscreen mode

πŸƒ Browser Support for Proxy

As you can see most of latest browsers already support Proxy except IE, Baidu and Opera. So if you do not care about these three, you can use it like a breeze.

Proxy browser support


🦾 Practical Usage

You might be thinking, hmmmmm... this ok but what is the practical usage of this. During my research for this article I came across an example of a JavaScript framework that is utilising the powers of Proxy and that frameworkkkkkk isssss....

  • Vue 3 : Vue 3 uses Proxy to be reactive and yes you got it right, Vue 3 does not support IE 😝. Vue 3 uses Proxy for change detection and firing side effects.

If you are not tired after reading my blog I will highly recommend you watch this free video to see full potential of Proxy.

You can play with my solution here


🌳 What to read next

Thank you for reading.

Follow me on twitter

πŸ––πŸΌ References

Latest comments (3)

Collapse
 
sarathsantoshdamaraju profile image
Krishna Damaraju

TIL, Wonderful write up Vikas. πŸ‘

Collapse
 
polaroidkidd profile image
Daniel Einars

This is an amazing write up! I really wonder how the interviewer came by this knowledge

Collapse
 
thejsdeveloper profile image
Vikas yadav

He might have read somewhere too. 😜