top of page

The Amazing Power of JavaScript Proxies

Updated: Jun 2, 2023

The Proxy object serves as a powerful tool that allows you to create a proxy or wrapper around another object. This proxy enables you to intercept and redefine fundamental operations for that object, providing a means to control and customize its behavior.


To make the concept more approachable, let's simplify the explanation:


The Proxy object allows you to wrap a target object, giving you the ability to intercept and redefine its essential operations. Think of it as creating a "hidden" gate around the desired object, through which all access to the object must pass. This gives you the opportunity to exert control and manipulate the object's behavior as needed.


It's worth mentioning that Proxy is not only a feature of JavaScript but also a software design pattern. Exploring more about this pattern can provide valuable insights.


JavaScript Proxies

To create a Proxy, you need two parameters:

1. target: This refers to the original object you want to wrap with the proxy. It becomes the proxy's underlying object.

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

2. handler: This is an object that defines which operations should be intercepted and how to redefine those intercepted operations. It can be thought of as a collection of "traps" for different operations.

const handler = {};

With the target and handler defined, you can create a Proxy instance by combining them:

const proxy = new Proxy(target, handler);

It's important to note that while Proxies are supported in most modern browsers, there are a few older ones (like IE) that do not support them.


Now that we have a grasp of what Proxies are, let's explore the possibilities and capabilities they offer.


Proxies In Action

Let's take a look at Proxies in action with a practical example. Imagine we are either a bank or a concerned girlfriend who wants to be notified every time the bank account balance is accessed. We can achieve this using a simple operation or trap provided by the Proxy called get.


In the following example, we have a bankAccount object that contains a balance of 2020 and the account owner's name, "John Master."

const bankAccount = {
    balance: 2020,
    name: 'John Master'
};

To implement the desired behavior, we define a handler object that includes the get operation or trap. The get operation is a function that takes three parameters: target, prop, and receiver. Within this function, we check if the accessed property is "balance." If it is, we log a notification with the current balance and the account owner's name. Finally, we return the value of the target[prop].

const handler = {
    get: function(target, prop, receiver) {
        if (prop === 'balance') {
            console.log(`Current Balance Of: ${target.name} Is: ${target.balance}`);
        }
        return target[prop];
    }
};

Now, we can create a proxy by combining the bankAccount and handler using the Proxy constructor:

const wrappedBankAccount = new Proxy(bankAccount, handler);

When we access the balance property of the wrappedBankAccount, the get operation will be triggered. In this case, it will log the current balance and return the actual balance value.

wrappedBankAccount.balance; // access to the balance

// OUTPUT:
// Current Balance Of: Georgy Glezer Is: 2020
// 2020

In the output, we can observe that as soon as the balance property is accessed, we are notified through the log statement. This behavior is made possible by utilizing the Proxy feature and setting up the get operation or trap.


Let's continue with our bank scenario, where we want to be notified whenever someone withdraws money from the bank account. Additionally, we have a constraint that the bank does not allow a negative balance. To achieve this, we will utilize the set handler or trap.


Here's the code example:

const bankAccount = {
    balance: 2020,
    name: 'John Master'
};

const handler = {
    set: function(obj, prop, value) {
        console.log(`Current Balance: ${obj.balance}, New Balance: ${value}`);

        if (value < 0) {
            console.log(`We don't allow Negative Balance!`);
            return false;
        }
        obj[prop] = value;

        return true;
    }
};

const wrappedBankAccount = new Proxy(bankAccount, handler);

wrappedBankAccount.balance -= 2000; // access to the balance
console.log(wrappedBankAccount.balance);

wrappedBankAccount.balance -= 50; // access to the balance
console.log(wrappedBankAccount.balance);

In this example, we are notified about the current balance and the new balance after a withdrawal. If the new balance is going to be negative, we log a message stating that negative balances are not allowed, and we abort the withdrawal action by returning false.


We are utilizing the set handler or trap, which is a function that returns a boolean value (true or false) indicating whether the update operation succeeded or not. It receives the following parameters:

  • target: The object that is being accessed (the object we wrapped).

  • prop: The property that is being accessed. In our example, it is "balance".

  • value: The new value that should be updated.

  • receiver: The object to which the assignment was originally directed. This is usually the proxy itself, but a set() handler can also be called indirectly, via the prototype chain or other means.

You can see that the set operation is similar to the get operation, with the addition of the value parameter representing the new value being assigned.


In the output, you can observe that when we attempt to withdraw a large amount, the withdrawal is aborted due to the negative balance constraint. The previous balance remains unchanged.


This example demonstrates how Proxies can be used to enforce constraints and provide customized behavior when updating object properties.

Who Uses Proxies

Proxies are widely used in many popular libraries and frameworks, harnessing their powerful capabilities to provide enhanced functionality and convenience. Here are a few examples of libraries that make use of Proxies:

  1. MobX: MobX is a state management library commonly used with React applications. It utilizes Proxies to enable automatic change tracking and reactive updates, allowing for efficient and optimized re-rendering of components.

  2. Vue: Vue.js is a progressive JavaScript framework for building user interfaces. Vue leverages Proxies to provide its reactivity system, which efficiently tracks changes to the underlying data and automatically updates the view accordingly.

  3. Immer: Immer is a library that simplifies immutable state management. It utilizes Proxies to create a draft state, allowing you to write code that appears to directly mutate the state, while actually producing a new immutable state under the hood.

These are just a few examples, and many other libraries and frameworks make use of Proxies to enhance their functionality and provide powerful features to developers.

Use Cases And Examples

As for use cases and examples, we have already explored a couple of them:

  1. Logging: As demonstrated in the previous examples, Proxies can be used to log or notify events such as accessing or modifying object properties. This can be valuable for debugging purposes or tracking important actions within your application.

  2. Validations: Proxies can enforce constraints and validations on object properties. In the bank account example, we used the set handler to prevent negative balance updates. This illustrates how Proxies can be used for data validation and ensuring data integrity.

These are just a glimpse of the various use cases where Proxies can be employed. The flexibility and versatility of Proxies make them a powerful tool for customizing and extending the behavior of objects and data structures in JavaScript applications.


Example: Caching Calculations

We use the get handler to check if the accessed property is "dollars". If it is, we compare the current balance with the cached balance. If they are equal, we return the cached value; otherwise, we calculate the value and update the cache. This way, subsequent accesses to the "dollars" property will retrieve the cached value instead of recalculating it.

const bankAccount={
    balance: 10,
    name: 'John Master',
    get dollars(){
        console.log('Calculating Dollars');
        return this.balance*3.43008459;
    }
};

let cache = {
    currentBalance: null,
    currentValue: null
};

const handler = {
    get: function(obj,prop){
        if(prop==='dollars'){
            let value = cache.currentBalance !== obj.balance ? obj[prop] 
                                                    : cache.currentValue;
            cache.currentValue=value;
            cache.currentBalance=obj.balance;
            
            return value;
        }
            return obj[prop];
    }
};

const wrappedBankAcount = new Proxy(bankAccount,handler);

console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);

// OUTPUT:
// Calculating Dollars
// 34.3008459
// 34.3008459
// 34.3008459
// 34.3008459

In this example, we introduce a property called "dollars" to the bankAccount object. Whenever the "dollars" property is accessed, we calculate the equivalent value in dollars based on the balance. However, since calculating can be a heavy operation, we want to cache the result as much as possible.

Example: DOM Manipulation

We define a helper function objectWithDom that takes an object and a DOM element ID. Inside the set handler, we update the value of the property, and then we update the corresponding DOM element by setting its innerHTML to the updated text from the text property of the object.

const bankAccount={
    balance: 2020,
    name: "John Master",
    get text(){
        return`${this.name} Balance Is: ${this.balance}`;
    }
};

const objectWithDom = (object,domId)=>{
    const handler = {
        set: function(obj,prop,value){
            obj[prop] = value;
            
            document.getElementById(domId).innerHTML=obj.text;
            
            return true;
        }
    };
    
    return new Proxy(object,handler);
};

// create a dom element with id: bank-account
const wrappedBankAccount = objectWithDom(bankAccount,"bank-account");

wrappedBankAccount.balance=26;
wrappedBankAccount.balance=100000;

In this example, we have a bankAccount object representing a bank account with a balance and a name. We want to update the text displayed on the screen whenever there is a change to the balance. We achieve this by using the set handler.


By wrapping the bankAccount object with the objectWithDom function, any changes to the balance property will automatically update the corresponding DOM element on the screen.


Summary

we have explored ECMAScript 6 Proxies and discovered their versatility and usefulness. Proxies are a remarkable tool that can be employed for various purposes. With Proxies, you have the flexibility to customize and control object behavior, enabling you to tailor it to your specific needs.


Proxies are an amazing tool in the JavaScript ecosystem, and their potential is only limited by your imagination and the specific needs of your projects. Embrace the power of Proxies and explore the possibilities they offer to enhance your JavaScript applications.

0 comments

留言


bottom of page