Proxies and Generators in JavaScript

October 30, 2019 0 Comments

Proxies and Generators in JavaScript

 

 

Let’s take a dive at two constructs that were introduced in the JavaScript ES6 specification:

Table of Contents

A solid understanding of these constructs will prove handy when working with JavaScript at a relatively low level. In this article, we will go over certain use cases where these constructs fit right in and will definitely save you several keystrokes.

Proxies

A Proxy, in simple terms, is an object that controls access to another object. According to the MDN docs:

The Proxy object is used to define custom behaviour for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

This description roughly translates into the idea that we can intercept fundamental operations (e.g function invocation, property lookup, assignment, enumeration, etc) on objects and functions and perform our own custom operations during run-time! Awesome, yeah?

Before we proceed, there are three key terms associated with the Proxy object:

  • Handler — the placeholder object which contains the trap(s).
  • Traps — the method(s) that provide property access. This is analogous to the concept of traps in operating systems.
  • Target —  the object which the proxy virtualizes.

When used correctly, the proxy object will powerfully complement its target object.

Creating a Proxy

Essential Reading: Learn React from Scratch! (2019 Edition)

We can create a JavaScript Proxy using the Proxy constructor — new Proxy().The Proxy constructor takes in two objects as parameters. The first is the target object the proxy virtualizes, and the second is the handler object which contains a set of methods called “traps.” These traps will govern property access to the target object:

const proxy = new Proxy(targetObject, handler);

We can create a simple Proxy object by passing a target object and an empty handler to the Proxy constructor:

const targetObj = {name: 'Target'};
const proxy = new Proxy(targetObj, {}); proxy.name; // returns 'Target'

The Proxy object we defined above currently does nothing to the target object. It just passes the request for the “name” property to the target object. In order to define one or more custom behaviours on the proxy for its target, we will need to declare one or more handlers.

Learn more about traps and handlers.

One of such handlers is the get trap handler. This example below intercepts “getter” calls to properties on the target object:

const data = { firtName: 'Bryan', lastName: 'John'
}; const handler = { get(target, prop) { return prop in target ? target[prop] : 'This property doesn’t exist, sorry'; }
}; const proxy = new Proxy(data, handler); console.log(proxy.firstName); // Returns 'Bryan'
console.log(proxy.age); // Returns 'No such property'

In the code above, we have the handler object which contains a get trap. The get trap intercepts the requests to access the target object, made by the proxy object, and returns the requested property if it is available or “This property doesn’t exist, sorry” if it's not.

If we want to intercept calls to set a property on an object, we will need to use the set trap.

Let’s look at a more useful example. We will use the set trap to check if the actualPay property on an object is set. If this property exists, we will deduct 3% from the amount paid as the transaction fee and assign the new value to the actualPay property:

const transaction = {}; const handler = { set(target, prop, value) { if(prop === 'actualPay' && typeof value === "number" && value > 0) { value = value * 0.97; } target[prop] = value; }
}; const proxy = new Proxy(transaction, handler);
proxy.actualPay = 1000;
console.log(proxy.actualPay); //Returns '970'

A full list of proxy traps and their sample usages can be found here in the MDN docs.

Example Use Cases

A benefit with proxies is that you don’t need to know or define the properties beforehand. This is in contrast to the ES5 getters/setters which requires the availability of the properties beforehand:

const data = { _firstName: 'John', _lastName: 'Doe', get firstName() { console.log('getting the firstname: ', this._firstName); }, get lastName() { console.log('getting the lastname: ', this._lastName); },
}; data.firstName; //logs -> getting the firstname: John
data.lastName; //logs -> getting the firstname: Doe

In the example above, we defined getters for the firstname and lastname properties. However, if we add a new property — age — we will need to define a new getter — get age() — on the data object to access that property:

data.age = 23; // adds a new property -- age
console.log(data.age); // logs 23 but doesn't automatically have a getter

With Proxies, we can simply register a get trap for all requests to access the properties of the object, including those that are weren’t declared at author time:

 const proxyObj = new Proxy({ firstName: 'John', lastName: 'Doe',
}, { get(targetObj, property) { console.log(`getting the ${property} property: ${targetObj[property]}`); }
}); proxyObj.firstName; //Returns -> getting the firstName: John
proxyObj.lastName;// Returns -> getting the lastName property: Doe
proxyObj.age = 23; console.log(proxyObj.age);// Returns -> getting the age property: 23

In the example above, we are able to log the values of all the properties on the object using a Proxy.

There are many more use cases where Proxies will suffice, for instance, we could create a custom object validator that checks an object’s properties to make sure that only intended types can be set as values.

We could also create a custom authentication system that ensures that the client is authorized to perform operations on the target. The possibilities are endless!

A few more possible uses cases for Proxies are:

  • Conditional caching
  • Property lookup extensions
  • Value correction
  • Debugging

Generators

When a function is invoked, the JavaScript engine starts to execute the code from the top of the function to the bottom. This model of execution is called run to completion and it’s nice when you want your function to run just as it is defined.

However, there are times where you’d wish to pause the function’s execution, run some other snippet of code, then continue right where you left off. Generators are the answer to this wish!

What is a Generator?

In simple terms, a generator is a function that can stop midway and then continue from where it stopped.

Let’s consider this analogy - Imagine that you are working on your Todo list for the day and your boss politely asks you to immediately work on something else. I bet your next actions, summarized in five steps, would be:

  • Implicitly remember where you left off on the Todo list.
  • Mumble something about your boss and kick a random chair.
  • Work on the task your boss just assigned to you.
  • Return to your desk and resume from where you left off on the Todo list.
  • Check your wrist to see if it’s 5 pm yet, you miss your new side project that’s definitely going to become a billion dollar company.

You just behaved like a generator function! I mean, except for bullet point 2 and 5. A generator function can pause its execution, run something else, and remember where it paused on its execution then continue from there.

Generator functions are ES6 constructs that can simplify the asynchronous control flow of JavaScript applications while implicitly maintaining their internal state. When a generator function is called, it first creates an iterator object called a generator before executing the function’s code. This generator object, according to the ECMAScript specification:

Is an instance of a generator function and conforms to both the Iterator and Iterable interfaces.

The Iterable protocol provides a lot of flexibility in specifying how to iterate over values in an object using the for..of construct. The Iterator protocol defines a standard way for values in an object to be iterated over. The iteration can be achieved using the .next() method as we will see later in this article.

Info: The async/await construct is based on generators. You can learn more here.

Working with Generators

Generator functions are created using the function* syntax. Its values are generated by calling the next() method and execution can be paused using the yield keyword. Each time the function is called, it returns a new Generator object which can be iterated over once:

function* getCurrency() { console.log('the generator function has started'); const currencies = ['NGN', 'USD', 'EUR', 'GBP', 'CAD']; for (const currency of currencies) { yield currency; } console.log('the generator function has ended');
} const iterator = getCurrency(); console.log(iterator.next()); // logs -> 'the generator function has started' // {value: 'NGN', done: false} console.log(iterator.next());
// {value: 'USD', done: false} console.log(iterator.next());
//{value: 'EUR', done: false} console.log(iterator.next());
//{value: 'GBP', done: false} console.log(iterator.next());
//{value: 'CAD', done: false} console.log(iterator.next());
// the generator function has ended
// {value: undefined, done: true}

In the example above, the generator function getCurrency()sends out data using the yield keyword. Calling the next() method on the generator object returns it’s yield value and a Boolean — done — which becomes true after all the values of the generator function have been iterated over, as seen in the last iteration in the example above.

We can also use the next() method to pass down data to the generator function:

function* displayCurrrency() { console.log(`currency info to be sent into the generator function: ${yield}`);
} const iterator = displaycurrency();
iterator.next(); //this starts the generator function
iterator.next('US dollars');
// logs -> currency info to be sent into the generator function: US dollars

In the example above, next('US dollars') sends data into the generator function and “replaces” the yield keyword with 'US dollars.'

There’s a lot more to Generators and we have only had a scrape at the surface here. You can learn more about Generators here.

Conclusion

We’ve had a look at JavaScript Proxies and how they can control and customize the behaviour of JavaScript objects. We also saw that a generator object can send data in and out of its generator function.

When used correctly, these ES6 constructs can greatly improve code architecture and design.

Like this article? Follow @JordanIrabor on Twitter


Tag cloud