Quantcast
Channel: Stories by Uday Hiwarale on Medium
Viewing all articles
Browse latest Browse all 145

Introduction to “Proxy” API for Metaprogramming in JavaScript

$
0
0

METAPROGRAMMING IN JAVASCRIPT

In this lesson, we are going to learn about the `Proxy` class available in the ES2015+. It provides a mechanism to intercept basic JavaScript operations on objects.

(source: unsplash.com)

In the earlier lesson, we learned about some metaprogramming concepts one of which is intercession. Intercession stands for intervening in an operation or intercepting an operation. For example, if you trying to get the value of an object’s property, but that operation was intercepted by some JavaScript program and a false or modified value was returned instead.

Intercession is like a man-in-the-middle attack where data is transformed in-flight between the source and the consumer but done deliberately. The program that sits between the source and the consumer is called Proxy.

You may have heard about this term while connecting to a remote server through a proxy server that sits between you and the remote server. Here the proxy server is able to hide your identity from the remote server as well as transform data received from the remote server if necessary.

JavaScript gives us the Proxy class in ES2015+ that helps us create a proxy around an object. The Proxy global object is a constructor function (class) and its constructor signature is as follows.

var proxy = new Proxy(target, handler);

Here, the target is the object for which a proxy needs to be created. The proxy is the object that will be used to perform some operations on the target. For example, proxy.prop = 1 operation would be handled by the handler and its job is to set the value of target.prop to 1.

💡 Both target and handler objects are required and must be descendents of Object and not a primitive value (including null and undefined). Else, TypeError: Cannot create proxy with a non-object as target or handler error is thrown.

Creating a proxy around target doesn’t prevent one from accessing the target but it should be kept private. The handler object contains specific methods one of which will be invoked by JavaScript when a certain operation is performed on the proxy and that method would be responsible to communicate with the target.

In the Reflect lesson, we learned about the internal slots and internal methods. Every proxy object has two internal slots viz. [[ProxyTarget]] and [[ProxyHandler]]. When we create a proxy object with new Proxy(), these slots are filled with target and handler arguments respectively.

(source: ECMAScript 2015 Table 5)

In the Reflect lesson, we also learned about the essential internal methods of all objects and saw a table full of internal methods (table above). These internal methods are trigged by Reflect’s static methods. Guess what, a proxy object also has these exact same internal methods as shown in the table below. These method signatures are as same as the above table.

(source: ECMAScript 2015 Table 30)

When the proxy’s internal method is called, it calls the target’s internal method with the exact same name. For example, if proxy[[Get]]() is called when proxy.prop is accessed (prop is just a property name), it will trigger target[[Get]]() method. This is called no-op forwarding since proxt’s is not performing any operation of its own.

However, we can provide an implementation of the internal method of the proxy using the handler. In the right-side column of the above table, we have the method (property) names that will be used to trap the internal method call of the proxy object. For example, handler.get method would trap the [[Get]] internal method call.

These handler methods are called traps as they trap or intercept the native operation of the proxy, therefore the native operation of the target. This method receives target as the first argument and the rest of the arguments are received as per the internal method specification.

If you read the Reflect lesson, then you know that the Reflect’s static method calls the internal methods of the target. So the Reflect.get(target, prop, receiver) calls the target[[Get]](prop, receiver) internal method which returns the value of the prop property of the target object.

A trap receives arguments as you would call a Reflect method with the same name. So in this case, handler.get trap would receive target, prop, and receiver as the argument. Now it is up to you to communicate with the target and return the result back.

This also means that you can call an equivalent Reflect method from within a trap and pass all the arguments to the Reflect method without even looking. This is the reason why Reflect static method and Proxy’s handler methods (traps) share the same method signature.

So if a trap is missing, then internal methods of the proxy object gets called which calls the internal method of the target and the proxy would behave as in there is no proxy at all. Meaning proxy.prop is equivalent to calling Reflect.get(target, prop) which is somewhat equivalent to target.prop.

If a trap is provided on the handler, you intercept the operation on the target such as if handler.get is present, so proxy.prop would execute the handler.get method and you can communicate with the target using a native approach or use the Reflect.get.

Therefore, for every Reflect static method name, there is an equivalent trap for the handler. You can just go through the Reflect lesson and understand the method signature of the Reflect’s static method because these would be method signature of the traps available in the handler. So we are not gonna go through them in this lesson and jump straight to examples.

💡 You can find the list of all supported methods on this MDN documentation but it would be the same as Reflect’s static method signatures.

The get and set traps

Let’s create a proxy for a simple object that contains name and age properties. The name property contains an object with fname and lname properties to represent a person’s name. What we want to achieve make name property look like it is a string. Therefore when a user accesses it, it will come as a string and when the user sets a string value, the string value will be split between the fname and lname properties.

(proxy/get-set-traps.js)

In the above example, we have created a proxy for the target object with get and set traps. With this, whenever proxy encounters get operations using proxy.prop syntax or Reflect.get(proxy, ...), the get trap would be executed. Same goes for the set operation.

In the get trap, we have accessed the property value on the target returned a transformed response (when prop is name). In the case of set trap, we are updating property value in the target and returning a boolean because that’s the signature of the corresponding Reflect.set method call.

💡 You can also use {} as the handler value. In this case, none of the target operations will be intercepted by the proxy handler and it will be directly processed by internal methods of the proxy.
(proxy/get-set-traps.js)

From the logs, you can see that whenever we assigned a value to proxy, the respective set trap was called. Since there is no has trap on the handler, the "age" in proxy operation (or Reflect.has(proxy, "age")) would be carried out on the target without any intercession.

The preventExtensions trap

Using a proxy, you can prevent the target object getting injected with arbitrary properties. For this, we need to use the preventExtensions trap.

(proxy/preventExtensions.js)

In the above example, preventExtensions trap is called when Object.preventExtensions or Reflect.preventExtensions method is called on the proxy. Through this trap, we are freezing the target using Object.freeze() method (Object.seal) which will prevent the addition of any new properties to the target.

Though this works, it would change the behavior of the target as you can see in the above example, the target.salary operation was unsuccessful as the target was frozen by the proxy in an earlier call. I would recommend adding some logic inside the set trap to prevent any new property addition when proxy is non-extensible instead of mutating the target object.

The construct trap

You can use proxy as a means to enforce a singleton pattern.

(proxy/construct.js)

In the above example, we have created the PersonProxy for the Person class (constructor function) and we are intercepting the instantiation operation using the construct trap. Hence whenever new PersonProxy() call is made or Reflect.construct(PersonProxy, ...) call is made, construct trap is executed which would pass all the control to Reflect.construct.

Revocable proxy

A revocable proxy is an object containing proxy field which contains the actual proxy and revoke field which is a function which when called sets the the [[ProxyTarget]] and [[ProxyHandler]] internal slots of the proxy to null. Therefore, any further operations on the proxy would result in a TypeError.

var { proxy, revoke } = Proxy.revocable(target, handler);

The Proxy.revocable method returns a revocable proxy object. A revocable proxy would be ideal when a third-party API needs the access of target but you want to intercept operations performed by this third-party API on the target. In that case, you can only provide the proxy object.

This abstracts the target object from the third-part API. Once you do not want this third-part API to make any more changes to the target, you can just call the revoke method. Any further operations on the proxy would then result in a TypeError error.

(proxy/revocable-proxy.js)

As you can see from the above example, once revoke has been called, any further operations on the proxy would result in a TypeError exception, whether or not that operation has a trap in the handler.

(thatisuday.com / GitHub / Twitter/ StackOverflow / Instagram)

Introduction to “Proxy” API for Metaprogramming in JavaScript was originally published in JsPoint on Medium, where people are continuing the conversation by highlighting and responding to this story.


Viewing all articles
Browse latest Browse all 145

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>