top of page
Writer's pictureThe Tech Platform

What are ES6 Proxies and how to use them?



In computing terms, proxies sit between you and the things you’re communicating with. The term is most often applied to a proxy server — a device between the web browser (Chrome, Firefox, Safari, Edge etc.) and the web server (Apache, Nginx, IIS etc.) where a page is located. The proxy server can modify requests and responses. For example, it can increase efficiency by caching regularly accessed assets and serving them to multiple users.


ES6 proxies sit between your code and an object. A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object’s property.


The following terminology is used in relation to ES6 proxies:

  1. target The original object the proxy will virtualize. This could be a JavaScript object such as the jQuery library or native objects such as arrays or even another proxies.

  2. handler An object which implements the proxy’s behavior using…

  3. traps Functions defined in the handler which provide access to the target when specific properties or methods are called.


Syntax:

const proxy = new Proxy(target, handler)

Basic Example

Here is the simplest example of Proxy. We have a dictionary with English and Bulgarian translation of a few words. So what we want to do is get the Bulgarian translation. We can see that everything is working as normal and when we want a word that is not included in the dictionary we get undefined.

const dictionary=
{
    apple: 'Ябълка',
    peach: 'Праскова',
    orange: 'Портокал'
};

const proxy = new Proxy(dictionary,{});

console.log(proxy['peach']);// Праскова
console.log(proxy['grapes']);// undefined

What about instead of this we want to return an appropriate message. More, we can add validations on setters. Here is an example:

const dictionary = 
{  
    apple: 'Ябълка',  
    peach: 'Праскова',  
    orange: 'Портокал'
};

const handler = 
{  
    get: (target, prop) => 
    {    
        return target[prop] || 'This word is not translated yet';  
    },  
    set: (target, prop, val) => {    
        if (typeof val === 'string') {      
            target[prop] = val;     
        }  
    }
};

const proxy = new Proxy(dictionary, handler);

console.log(proxy['peach']); // Праскова
console.log(proxy['grapes']); // This word is not translated yet
proxy['grapes'] = 5
console.log(proxy['grapes']); // This word is not translated yet
proxy['grapes'] = 'Грозде';
console.log(proxy['grapes']); // Грозде

Dynamic API Example

We saw a basic example of using Proxy and how to redefine operations. But let’s see something a bit more interesting. Let’s create an API that can intercept method calls that don’t exist. For example we want to call an API with method that can include value or body represented as query and the proxy should return dynamically implement its functionality in runtime.

const { METHODS } = require('http');

const handler = {  
    get: (target, propName) => {  	
        const method = METHODS.find(method => 
        (propName.toLowerCase()).startsWith(method.toLowerCase()));		
            let propNameCopy = propName.slice(method.length + 1);		
            let index = propName.indexOf(propNameCopy.match(/[A-Z]/));		
            let path = index !== -1 ? 				
                    `/${propName.substring(method.length, 
                    index)}/${propName.substring(index)}`			
                    :			`
                    ${propName.substring(method.length)}`;				
            
            return (...args) => {				
                let queryOrBody = '';				
                if (args.length) {					
                    const firstVal = args.shift();					
                    typeof firstVal !== 'object' ? path += `/${firstVal}` 
                                                : args.unshift(firstVal);					
                    queryOrBody = args.shift() || {};					
                    if (Object.keys(queryOrBody).length && method === 
                    'GET') 
                    {						
                        path += '?';						
                        for (let key of Object.keys(queryOrBody)) {							
                            path += `${key}=${queryOrBody}&`;							
                        }						
                        path = path.slice(0, -1);					
                    }				
                }			
            console.log(`Method: ${method}, Path: ${path.toLowerCase()} 
            ${method === 'Post' ? (', Body: ' + JSON.stringify
                                            (queryOrBody)) : ''} `
            );		
        }  
    }
};

const api = new Proxy({}, handler);

api.getUsers(); // Method: GET, Path: /users
api.getUserLikes('98') // Method: GET, Path: 'user/likes/98
api.getUserPosts('98', {page: 2, name: 'JS'}); // Method: GET, Path: /user/post/98?page=2&name=js
api.postArticle({name: 'Title', description: 'Some text' }) // Method: POST, Path: /article, Body: {"name": "Title", "description": "Some text"}

Let me explain what the code is doing. First we are getting the type of the method which can be GET, POST, PATCH, DELETE. Then we want to get the endpoint of the method. After that we check if the method is GET and have query parameters, we add them to the endpoint if not, we check if there is body of the request. Finally we display the API call result.


Proxy Trap Types

We’ve seen the get and set in action which are likely to be the most useful traps. However, there are several other trap types you can use to supplement proxy handler code:

  • construct(target, argList) Traps the creation of a new object with the new operator.

  • get(target, property) Traps Object.get() and must return the property’s value.

  • set(target, property, value) Traps Object.set() and must set the property value. Return true if successful. In strict mode, returning false will throw a TypeError exception.

  • deleteProperty(target, property) Traps a delete operation on an object’s property. Must return either true or false.

  • apply(target, thisArg, argList) Traps object function calls.

  • has(target, property) Traps in operators and must return either true or false.

  • ownKeys(target) Traps Object.getOwnPropertyNames() and must return an enumerable object.

  • getPrototypeOf(target) Traps Object.getPrototypeOf() and must return the prototype’s object or null.

  • setPrototypeOf(target, prototype) Traps Object.setPrototypeOf() to set the prototype object. No value is returned.

  • isExtensible(target) Traps Object.isExtensible(), which determines whether an object can have new properties added. Must return either true or false.

  • preventExtensions(target) Traps Object.preventExtensions(), which prevents new properties from being added to an object. Must return either true or false.

  • getOwnPropertyDescriptor(target, property) Traps Object.getOwnPropertyDescriptor(), which returns undefined or a property descriptor object with attributes for value, writable, get, set, configurable and enumerable.

  • defineProperty(target, property, descriptor) Traps Object.defineProperty() which defines or modifies an object property. Must return true if the target property was successfully defined or false if not.


To summarize a proxy can be used wherever an object is expected, and the complex functionality that proxies provide with just a few lines of code makes it an ideal feature for metaprogramming.



Resource: Wikipedia, Medium (Svetloslav Novoselski)


The Tech Platform

0 comments

Comments


bottom of page