This blog post is part of the Mixmax 2016 Advent Calendar. The previous post on December 2nd was about Upgrading to Node 6 on Elastic Beanstalk.
The long-awaited stable LTS version of Node 6.x was released last October, and with it, many performance and security improvements, and new ES6 syntax features are now natively supported as well.
While some of the new ES6 features can be considered syntax sugar, they also open the door for much more concise and understandable code, as well as opening for metaprogramming capabilities. We will now explore some of these features and how it compares to “old” code.
Default arguments, spread operator and destructuring.
One common use case is to have a function that has multiple arguments where one or more of them are optional. Such a function would look like something similar to:
function makeSandwich(customer, bread, filling) {
bread = bread || 'wheat';
filling = filling || 'ham';
// Make the sandwich
}
We can make that code more concise and simpler to understand by using default arguments in our function:
function makeSandwich(customer, bread = 'wheat', filling = 'ham') {
// Make the sandwich.
}
How about if we pass our sandwich ingredients as an array?
function makeSandwich(customer, ingredients) {
const bread = ingredients[0];
const filling = ingredients[1];
// Make the sandwich
}
We can make the syntax more concise with the spread operator now!
function makeSandwich(customer, [bread, filling]) {
// Make the sandwich
}
If we want to retain the ability to keep default ingredients, then we can combine the two features like so:
function makeSandwich(customer, [bread, filling] = ['wheat', 'ham']) {
// Make the sandwich
}
Unfortunately, the above will only work as long as the second parameter is undefined, if we pass a value for bread but not for filling, then filling won't be defaulted to 'ham' as expected.
What if we want to keep our list of ingredients support open and allow for new ingredients later? Then we can use destructuring and rest !
function makeSandwich(customer, ...ingredients) {
const [bread, filling] = ingredients;
// Make the sandwich
}
Later when we can add toppings and sides to our sandwich, we can add that to our destructuring sentence:
function makeSandwich(customer, ...ingredients) {
const [bread, filling, toppings, sides] = ingredients;
// Make the sandwich
}
Maybe an array is not the best representation of our ingredients, we can use an object and still use destructuring to assign properties to local variables:
function makeSandwich(customer, ingredients) {
const { bread, filling, toppings, sides } = ingredients;
// Make the sandwich
}
An unoptimized quicksort-like algorithm that uses the spread operator to perform array concatenations looks like this:
function quicksort(list) {
const size = _.size(list);
if (size === 0) return [];
if (size === 1) return list;
const [pivot, ...rest] = list;
const [left, right] = _.partition(rest, item => item < pivot);
return [...quicksort(left), pivot, ...quicksort(right)];
}
Although not a real quicksort because it does not sorts in-place, it demonstrates how concise JavaScript code can now be thanks to destructuring and the spread operator in particular.
Map and Sets
The Map
and Set
objects are actually supported since Node 4.x, however some details were fleshed out until the Node 6.x releases.
Map
the Map
is a key/value data structure (similar to plain objects). A Map
, like a plain object, can store values identified with a key, but unlike an object, we can have some guarantees about these Map
and our keys:
- When iterating over our map, values are retrieved in insertion order.
- Unlike an object, a
Map
does not have extraneous keys from the prototype inheritance - They are iterable with
for .. of
- Provides useful functions to interact with the map, such as
Map#entries
,Map#keys
,Map#has
Map#forEach
, among others
// A Map can be instantiated with some initial values, pass an array with arrays whose
// first value is the key and the second is the value to store in the mappings
const map = new Map([
['one', 1]
['two', 2]
]);
map.set('three', 3);
map.has('two'); // true
map.has('three'); // true
map.has('four'); // false
map.forEach((key, value) => {
console.log("%s: %s", key, value);
});
for(const [key, value] of map) {
console.log("%s: %s", key, value);
}
console.log(map.get('one')); // 1
console.log(map.size); // 3
Set
The Set
is a particularly useful data structure, it has the property that the Set
is a list of values that can't be repeated. Similar to Map
, the Set
object can be interacted with similar methods
const set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set); // Set { 1, 2, 3 }
set.add(3);
console.log(set); // Set { 1, 2, 3 }
console.log(set.size); // 3
for (const item of set) {
console.log(item);
}
// You can create an array from a set with:
let arr = Array.from(set)
// ... or use the spread operator
arr = [...set];
Metaprogramming with ES6
One of the not-so-talked features of ES6 is the meta programming capability brought to the table thanks to Symbol and Proxy classes.
Proxy
The Proxy object adds the ability to intercept attribute access to the proxied object. For example, an interesting use case is given by the mongojs library, the Mongo database connection is exposed as a Proxy object where you can access your MongoDB collections as properties of said object. For example:
db.users.find({ /* ... */ })
Code to enable the above snippet could look something like:
const myCollections = require('path/to/my/collections');
const myConnection = require('path/to/my/db/connection');
const proxy = new Proxy(myConnection, {
get(conn, prop) {
if (Reflect.has(myCollections, prop)) {
return myCollections[prop];
}
return conn[prop];
}
});
module.exports = proxy;
Our proxy object has the target as its first argument, and as a second argument, a handler object that implements the intercepting functions which will be called when attempting to access an attribute in the proxy.
In this implementation, we define a get
function, which will intercept every single attribute access to the proxied object. In our implementation here, we use the new Reflect
class which provides several utilities functions to safely inspect other objects, here we check if the accessed property is defined in our collections object, if it is, then we return that, otherwise we delegate the call to the original object.
Symbol
The Symbol is a new data type, which has some interesting characteristics:
- It is not instantiable, you can create new symbols with:
const foo = Symbol(‘foo’);
- Two defined symbols are never equal:
const a = Symbol(‘foo’);
const b = Symbol(‘foo’);
a === b // false
Properties defined with symbols are not enumerable:
const foo = {
one: 1,
two: 2
};
const three = Symbol('three');
foo[three] = 3;
Object.keys(foo); // ['one', 'two']
Symbols, then, can be used as special object properties that “hide” some data inside the objects. Note that, a list of Symbols in an object can still be accessed with Object.getOwnPropertySymbols()
so these properties are not completely hidden, but merely separated and handled differently than regular properties.
You want to use a Symbol when you want to store object metadata that you don’t want to expose if you intend your object to be iterated with for … of
loops, or properties that you want to hide when serializing with JSON.stringify
.
Another very interesting use case is for implementing interfaces in classes. Javascript exposes well-defined Symbols, one of the most approachable ones is the Symbol.iterator
symbol, if your class implements a generator function defined as the Symbol.iterator
property, then your class can be looped with for..of
and expanded with the spread operator “...”
Let’s make a very silly example, we’ll implement a random iterator where, given an initial list of values, it iterates them at “random”.
function random(a, b) {
return Math.floor(Math.random() * (b - a + 1)) + a;
}
class RandomIterator {
constructor(values) {
this.values = values;
}
*[Symbol.iterator]() {
const values = this.values.slice(0);
while (values.length > 0) {
const next = random(0, values.length - 1);
const [value] = values.splice(next, 1);
yield value;
}
}
}
const iterator = new RandomIterator([1,2,3,4,5]);
// This will print the values on our RandomIterator in random order.
for(const i of iterator) {
console.log(i);
}
// You can also use the spread operator!
[...iterator] // Will print the list in the iterator in random order
[...iterator] // Calling multiple times will return the items in different order.
Symbols can open up several ways on how you can interact with your own data structures using plain javascript code, also, using Proxies and Symbols you can create a whole new level of meta programming that was not available until now!
Conclusion
The new syntax introduced with ES6 allows to express ideas and abstractions in a more concise way. Symbols for example open up the idea of mixins implemented in a different way. Default args make it clearer the signature of a function without checking the implementation. Spread and Destructuring makes it easier to work with arrays and objects. The Map and Set classes give a better idea of an intention of some variable (A map is a key/value pair, a set is a list of items without duplicates) and so on. Many of these ideas could be implemented with ES5 code, but the new syntax makes it so that these ideas and intentions are explicit and clear by just looking at the code.
Do you want to take advantage of the new syntax offered under Node 6.x? Come join us.