Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
October 29, 2021 12:27 pm GMT

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 

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}

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 

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 trackedenhancedObj.age = 30; 

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 canforward these operation to the wrapped object.

Proxy Image

Anatomy of Proxy

let proxy = new Proxy(target, handler)

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

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 MethodHandler MethodTriggered On
[[Get]]getWhen reading a property
[[Set]]setWhen writing a value to a property
[[HasProperty]]hasWhen used with in operator
[[Delete]]deletePropertyWhen deleting a property with delete operator
[[Call]]applyWhen 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 nameproxy.name = "morty";// Log is MORTY instead of mortyconsole.log(character.name); // MORTY

[[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 

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 methodReflect callInternal 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);  }});

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 

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


Original Link: https://dev.to/xenoxdev/javascript-jungle-who-is-the-proxy-3a3b

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To