Javascript

Proxy Objects Features In Javascript ES6

Proxy Objects Features In Javascript ES6

One of the unique features in ES6 is the addition of proxy objects. The proxy object is an object that wraps another object to defined particular operations on that object.

 

 

 

If you heard about the term interception before in other languages or tools. A proxy object acts as an interceptor that wraps another object which intercepts a set of operations like reading that object or writing to that object. The term “proxy” because it forwards the calls to the target object. Many frontend frameworks and technologies uses the Proxy object like Vuejs.

These operations are somewhat called “traps” and it much like magic methods in languages such as PHP which trigged automatically when specific event happens, for example when setting a property to a proxy object the set() trap is triggered.

 

Terminology of Proxy Object

const proxy = new Proxy(target, handler);
  • target – The original object to wrap, can be an object, a function or even class.
  • handler– The object that holds the operations (traps) that control the behavior of the proxy in react to specific event.

 

Basic example:

const user = {
    firstName: "John",
    lastName: "Doe",
    age: 35
}

let proxy = new Proxy(user, {});
            
console.log(proxy);

Proxy Objects And Traps In Javascript - proxy debug

As you i created a proxy using the new Proxy() constructor and passing the target object and the handler. The handler can be empty as in this example and any operation performed on the Proxy forwarded to the original object.

For example we can read and write to the proxy as if it is the original object:

console.log(`My name: ${proxy.firstName} ${proxy.lastName}`);
// My name: John Doe
proxy.firstName = 'Wael';
            
console.log(proxy.firstName);   // Wael
console.log(user.firstName);    // Wael

As you see here reading and writing to the proxy is the same as reading and writing to the target object. So what’s the point of using Proxy? The point is by customizing the proxy behavior using operations or traps, let’s see this below.

 

Proxy Traps

Proxy objects takes it’s power from the operations defined in the handler object, these operations called “traps”. From these operations:

  • get(): The get() trap triggered automatically when a property of the target object accessed via a proxy object.
  • set(): The set() trap triggered when attempting to set a property of the target via proxy.
  • apply(): The apply() trap is triggered when attempting to make a function call, it’s like javascript’s apply() function.

 

get()Trap

Using the previous example let’s define a handler with the get() trap:

const handler = {
        get(target, property, receiver) {
            console.info(`${property} accessed`);
            return target[property];
        }
};

let proxy = new Proxy(user, handler);
            
console.log(`My name: ${proxy.firstName} ${proxy.lastName}`);

// output
firstName accessed
lastName accessed
My name: John Doe

As you see in the output two console statements has been output when reading both proxy.firstName and proxy.lastName. The get() trap accepts three parameters (target, property, receiver). The “target” represents the original object and property represents the accessed property via the proxy. 

The typical implementation to retrieve property value is by using target[property]. In fact this is the default implementation when no get() trap defined in the handler.

Let’ customize the get() trap by returning an arbitrary string:

get(target, property, receiver) {
    console.info(`${property} accessed`);
    return "Annonymous";
 }

// output
My name: Annonymous Annonymous

Or you can check for a property:

get(target, property, receiver) {
    console.info(`${property} accessed`);
    if(property === 'firstName') {
        return "Wael";
    } else if(property === 'lastName') {
         return "Salah";
    }
    return "Annonymous";
}

The benefit of the get() trap on proxy objects is to to generate computed properties. For example if we want to get the formatted user data as a string in the previous example:

get(target, property, receiver) {
    if(property === 'userDetails') {
        return `First name: ${target['firstName']}\nLast name: ${target['lastName']}\nAge: ${target['age']}`;
    } else {
            return target[property];
    }
  }


console.info(proxy.userDetails);
console.info(proxy.firstName);

// output
First name: John
Last name: Doe
Age: 35

John

Example: Prevent access to particular property:

const user = {
    ....
        ....,
        balance: '100$'     // protected property
}

get(target, property, receiver) {
     if(property === 'balance') {
        throw new Error(`Attempt to access protected property ${property}`);
      } else {
        return target[property];
      }
 }

console.info(proxy.balance);  

// output
Uncaught Error: Attempt to access protected property balance

 

set()Trap

The set() trap fired when attempting to set a property on proxy.

Let’s add a set() trap to the previous example:

let handler = {
    set(target, property, value) {
       target[property] = value;
                   
       return true;
    }
};

This is the simplest form of the set() trap which is to the set the value over the property on the target. The set() trap accepts there parameters (target, property, value) which much the same as the get() trap. Most important thing to note that the set() must return true on successful write and false or exception on unsuccessful write.

Of course this is trivial use of set() trap. The most common use of the set() trap is by doing validations on the target object.

Suppose that we want to validate the user data using specific constraints:

let handler = {
   set(target, property, value) {
               
                   if(property === 'age') {
                      if(isNaN(value)) {
                        throw new Error(`age must be number`);
                      }
                      
                      if(value > 40) {
                          throw new Error(`age must not be bigger than 40`);
                      }
                      
                      if(value < 20) {
                          throw new Error(`age must not be less than 20`);
                      }
                   }
                   
                   if(property === 'firstName' || property === 'lastName') {
                       if(value.length > 30) {
                          throw new Error(`${property} length must not exceed 30 characters`);
                       }
                   }
               
                   target[property] = value;
                   
                   return true;
               }

}
proxy.age = 'not number';
proxy.firstName = 'too long stringggggggggggggggggggggggggggggggggggggggggggggggggggggggg';			

console.log(proxy.age);
console.log(proxy.firstName);

// output
Uncaught Error:  age must be number
Uncaught Error: firstName length must not exceed 30 characters

Also using the set() trap we can disallow writing to specific property on the target object the same as get().

So in the above example suppose we want to prevent setting the user balance, we can easily add appropriate logic like so:

set(target, property, value) {
               
                   if(property === 'balance') {
                   	throw new Error(`Attempt to set protected property ${property}`);
                   }

                   target[property] = value;
                   
                   return true;
               }

 

apply()Trap

The apply() trap is function call trap, which means when the target in this case is a function object. It fired when attempting to call the proxy as a function or calling the native js apply() function on the proxy.

const handler = {
  apply: function(target, thisArg, argumentsList) {
  }
};

The apply() trap accepts three arguments (target, thisArg, argumentsList).

Example: sum numbers using apply():

let handler = {

    apply(target, thisArg, argumentsList) {
        console.log(argumentsList);
        return argumentsList.reduce((prev, a) => prev + a, 0);
    }
}

const sumProxy = new Proxy(function() {}, handler);

console.log(sumProxy(10, 20, 30));

In this example we have access to arguments list passed when calling the proxy as a function, As you see i applied the javascript reduce() function on the array of arguments to do the sum operation.

 

Example2: using the previous user example, By having knowledge that the user object have a balance property and we want to increment or decrement this balance using apply():

const user = {
    	firstName: "John",
    	lastName: "Doe",
    	balance: 1000
}

function incrementBalance(user) {
    user.balance++;

    return user.balance;
}

function decrementBalance(user) {
    user.balance--;
    
    return user.balance;
}

let handler = {
   apply(target, thisArg, args) {
       return target(...args);
   }
}


let incrementProxy = new Proxy(incrementBalance, handler);
let decrementProxy = new Proxy(decrementBalance, handler);


console.info(incrementProxy(user));
console.info(incrementProxy(user));

console.info(decrementProxy(user));
console.info(decrementProxy(user));
console.info(user);

// output
1001 
1002 
1001 
1000 
Object { firstName: "John", lastName: "Doe", age: 35, balance: 1000 }

 

deleteProperty()Trap

The deleteProperty() trap is a trap fired when calling the delete operator on proxy object to remove specific property. Again we can use this to allow or disallow deleting properties on the target object.

const proxy = new Proxy({}, {
  deleteProperty: function(target, prop) {
    if (prop in target){
      delete target[prop];
      console.info(`property deleted: ${prop}`);
      return true;
    }
    else {
      console.error(`property not found: ${prop}`)
      return false;
    }
  }
});


delete proxy.foo;    

proxy.foo = 'bar';
delete proxy.foo;

// output
property not found: foo 
property deleted: foo

The deleteProperty() trap accepts two arguments (target, prop). You should add the appropriate to delete a property like in the example above. The trap must return a boolean value indicating successful delete or error.

 

has()Trap

The has() trap fired when using the in operator to check for property existence on an object.

A typical example when you have a protected property starting with _ to hide it from specific users.

const handler = {
  has(target, prop) {
    if (prop[0] === '_') {
      return false;
    }
    return prop in target;
  }
};

const user = {
  _password: '*****',
  views: 100
};

const proxy = new Proxy(user, handler);

console.log('views' in proxy);
console.log('_password' in proxy);

// output
true
false

The has() trap accepts two arguments (target, prop). As you see when using the in operator to check for property on the proxy the has() trap is intercepted and appropriate logic applied. This trap must return a boolean indicating for property existence or not.

 

Other Traps You Can Check

 

Proxy Objects and Plain Objects

Proxy objects can be converted back to the plain version of this object using several techniques, for example the destruction and spread operator can be used:

const plainObject = { ...proxyObject };

Also other solutions is to use JSON.parse() function:

JSON.parse(JSON.stringify(proxyObject));

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments