Month: December 2018

javascript proxy – Querying arrays with more readable methods

javascript proxy

javascript Proxy.

This a new feature introduced in the ES6 standard. The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

The basics.

There are 3 key terms we need to define before we proceed:

  • handler — the placeholder object which contains the trap(s).
  • traps — the method(s) that provide property access.
  • target — object which the proxy virtualizes.

Here is a list of available Traps.

  • apply
  • construct
  • defineProperty
  • deleteProperty
  • get
  • getOwnPropertyDescriptor
  • getPrototypeOf
  • has
  • isExtensible
  • ownKeys
  • preventExtensions
  • set
  • setPrototypeOf

The list going on and on…

The syntax

let proxy = new Proxy( target, trapObject );

Quick example.

Let’s see a quick example before we dive in into create our array query library.


class Student {
    constructor(first, last, scores) {
        this.firstName = first;
        this.lastName = last;
        this.testScores = scores;
    }
    get average() {
        let average = this.testScores.reduce( 
            (a,b) => a + b, 
            0 
        ) / this.testScores.length;
        return average;
    }
}

//instantiate the student class  
let john = new Student( 'John', 'Doe', [70, 80, 90] );

let johnProxy = new Proxy( john, {
   //we trap the get and print the property accessed 
    get: function( target, key, context ) {
        console.log( `john[${key}] was accessed.` );
        return john[key];
    } 
});


johnProxy.firstName;
//output.
//> john[firstName] was accessed.
//> <property content>

In this example we are just intercepting the properties called, for example johnProxy.firstName and printed it to the console, this could be used for debugging purpose in dev environments. We can do the same and intersect the ‘set’ and print a message to the console when we set the value on a property.

Let’s create our query library.

It would be similar to write an assertion library, where you can use the name of the field you want to query withing the method’s call instead of passing it as a parameter.

First let’s create the assertion object.

Here we are going to define the assertion operations, our proxy object will return the objects based on this conditions:

let assertions = {
  Equals: (object, value) => object === value,
  IsNull: (object, value) => object === null,
  IsUndefined: (object, value) => object === undefined,
  IsEmpty: (object, value) => object.length === 0,
  Includes: (object, value) => object.includes(value),
  IsLowerThan: (object, value) => object < value,
  IsGreaterThan: (object, value) => object > value,
  EndsWith: (object, value) => object.endsWith(value),
  StartsWith: (object, value) => object.startsWith(value),
}

The proxy object.

Here we going to trap the get, of the target object to read the name of the property and make sure it starts with findWhere then we use the object field name we want to query, then the assertion (from the assertions above), So the syntax will looks something like this arr.findWhereNameStartsWith('Jo')

let wrap = arr => {
  const prefix = 'findWhere';
  let assertionNames = Object.keys(assertions);
  return new Proxy(arr, {
    get(target, propKey) {
      if (propKey in target) return target[propKey];
      var assertionName = assertionNames.find(assertion =>
        propKey.endsWith(assertion));
      if (propKey.startsWith(prefix)) {
        var field = propKey.substring(prefix.length,
          propKey.length - assertionName.length).toLowerCase();

        var assertion = assertions[assertionName];
        return value => {
          return target.filter(item => assertion(item[field], value));
        }
      }
    }
  })
}

Declare the object.

Next step is to declare the object and wrap it up with the proxy.

let arr = wrap([
  { name: 'John', age: 23, skills: ['mongodb', 'javascript'] },
  { name: 'Lily', age: 20, skills: ['redis'] },
  { name: 'Iris', age: 43, skills: ['python', 'javascript'] }
]);

Calling the methods

console.log(arr.findWhereNameEquals('Lily')) // finds Lily.
console.log(arr.findWhereSkillsIncludes('javascript')) // finds Iris.

The code above the first line will return the object where Property “Name” is equal to “Lily” and print it to the console, and the second line will return the object(s) where “skills” property includes “javascript” within the array.

Full source code.

Use the following code to play more with javascript proxy.

Things to notice.

Notice how we call the a method in the array that we didn’t declared before arr.findWhereAgeIsLowerThan(30) we use the proxy to read this method, the expected syntax is findWhere + <array_field_name> + <assertion> this allow us to create custom queries using the array field name we want to query within the method name:

  • arr.findWhereAgeIsLowerThan(27);
  • arr.findWhereNameEquals(‘Lily’);
  • arr.findWhereSkillsIncludes(‘javascript’);

Wrapping up

With JavaScript proxy, you can wrap up an existing object and intercept any access to its attributes or methods. Even if they don’t exist!

There are many real-world applications for Proxies:

  • Create SDK for an API.
  • validation.
  • value correction.
  • Revoke access to an object property/method.
  • Querying arrays with more readable methods (the main topic of this post).
  • property lookup extensions.
  • tracing property accesses.
  • Monitoring async functions.

Conclusions.

Javascript proxy enables a lot of possibilities to create more readable APIs where you can combine field names, assertions and create dynamics methods on the fly, however as you can see in the link below IE doesn’t support it and this can be a downside to create code that needs to be supported by all major browsers. Unfortunately, it’s not possible to polyfill or transpile ES6 proxy code using tools such as Babel, because proxies have no ES5 equivalent.

For more info about javascript proxy and browser compatibility you can Learn more here