Published on

Proxy in JavaScript

Authors
  • avatar
    Name
    Ibrahim Motani

But what does Proxy even mean?

Before getting into any technical details, let us understand what does the word Proxy mean without any context? The word proxy means a person or a thing that represents someone else. The proxy also has the authority to act on behalf of another person or entity.

Now let us think of Proxy in terms of JavaScript. A Proxy is an object that can represent and act on behalf of it’s original object. The proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations on that object. These proxies can redefine fundamental operations like getting, setting and defining properties. Let us take a look at an example.

const target = {
  message1: "hello",
  message2: "world",
};

const handler = {
  Reflect.get(target, prop, receiver) {
    return "everyone";
  },
};

const proxy = new Proxy(target, handler2);

Let us understand the above code example and terminologies used when talking about Proxy:

  1. Target - The original object that you want to target or you want to create a proxy for. The target object in the above code example is the target here.
  2. Handler - Handler is an object which has methods that will intercept and modify the operations on the target object. These methods are also called traps. These methods redefine the intercepted operations.
  3. Reflect - The Reflect object was introduced in ES6 to provide an easier manner to build handler methods. Reflect object is a wrapper over internal JavaScript methods. We will understand more about these internal methods and the Reflect object as we move further in this blog.

Why do we need Proxy? What problem does it solve?

In my opinion, Proxy solves two major problems:

  1. Customisation

    Proxy enables customisation of the behaviour of objects. It addresses the problem of customisation by allowing us to modify fundamental operations on an object. This provides us a flexible mechanism to alter the behaviour of objects as per our needs. Let’s consider a scenario where you want to store only strings in an object and these strings should be stored in uppercase. This can be achieved by Proxy like the code below

    // the object for which the proxy will be created
    const target = {};
    
    // handler 
    const handler = {
    set(target, prop, value) {
        // intercept write operation and converts string to uppercase
        const uppercaseValue = typeof value === 'string' ? value.toUpperCase() : value;
        // returns a boolean to indicate if the write was successful
        return Reflect.set(target, prop, uppercaseValue);
    }
    };
    
    // create a proxy object with modified write operation
    const proxy = new Proxy(target, handler);
    
    // write
    proxy.name = "Ibrahim";
    proxy.city = "Jamnagar";
    
    console.log(proxy.name); // IBRAHIM 
    console.log(proxy.city); // JAMNAGAR
    

    Let us consider another scenario where we want to read a property from the object but it doesn’t exist. We don’t want it to return undefined. To achieve this, we can make use of Proxy and modify the [[Set]] method to achieve the same. Similar mechanisms can be used for data validation and input sanitization as well.

    // target
    const target = {};
    
    // customize write property in the handler
    const handler = {
    get(target, prop, receiver) {
        // if the property doesn't exist create it with a default value
        if (!(prop in target)) {
        target[prop] = "Naruto";
        }
    
        // continue with the default behavior for reading property
        return Reflect.get(target, prop, receiver);
    }
    };
    
    // create a proxy
    const defaultObject = new Proxy(target, handler);
    
    // read property
    console.log(defaultObject.name); // 'Naruto'
    
  2. Security and Encapsulation

    Direct access to object properties can lead to unintentional modifications. Proxy allows us to implement access control by setting policies. Let us take a look at an example where we don’t want the private properties of the object to be accessed by every part of the code. Let us see how to achieve it with Proxy

    const target = { _secretKey: 123, normalKey: 456 };
    
    const handler = {
      get(target, prop, receiver) {
        if (prop.startsWith("_")) {
          throw new Error('Access to private property denied');
        }
        return Reflect.get(target, prop, receiver);
      },
      
      set(target, prop, value, receiver) {
        if (prop.startsWith("_")) {
          throw new Error('Modification of private property denied');
        }
        return Reflect.set(target, prop, value, receiver);
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(proxy._secretKey); // throws an error
    console.log(proxy.normalKey); // 456
    

Reflect Object and Internal methods

So now we know that Proxy is an an object with trap functions that will intercept and modify the interactions with the target object. But what all calls can be trapped in a proxy is? For all operations/interactions with the object, there’s a corresponding internal method in JavaScript that describes how it works at the lowest level. For example, when we access the message1 propety from the target object like this target.message1, the internal [[Get]] method is called. Smilarly, when we want to assign a new value to the message1 property like target.message1 = ‘How are you?’ , the internal method [[Set]] is called.

Following is a list of internal methods that can be trapped in the handler parameter. The fist column is the name for internal method. The second column is the name of the method that we can use in the handler function to modify the internal methods. The third column will tell us at which interaction with the object the corresponding internal method is triggered.

Internal MethodHandlerTriggers when
[[Get]]get()Reading a property
[[Set]]set()Writing a property
[[HasProperty]]has()in operator
[[Delete]]deleteProperty()delete operator
[[Call]]apply()Function call
[[Construct]]construct()new operator
[[GetPrototypeOf]]getPrototypeOf()Object.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOf()Object.setPrototypeOf
[[IsExtensible]]isExtensible()Object.isExtensible
[[DefineOwnProperty]]defineProperty()Object.defineProperty, Object.defineProperties
[[GetOwnProperty]]ownKeys()Object.getOwnPropertyNames, for..in, Object.keys/values/entries

All of the above mentioned properties can be intercepted and modified according to our needs with the Reflect Object.

That’s all for today folks. Thank you for reading. See you in the next one 👋

References