Understanding the new features in JavaScript 2017 (aka ES8)

As you might know, ECMAScript is the language specification for one of the most popular implementations, commonly known as Javascript. It’s been receiving revision updates to its features, syntaxis elements, performances enhancements and others, year after year since 2015 with version 6 or ES6. This version is also knows as ES2015, the version where promises, arrow functions, and other modern language elements were introduced.
The specification version for 2017 is ES8, Standard ECMA-262, commonly known also as ES 2017.
It includes several new features that let you write cleaner code with less lines when working with promises, thanks to the addition of async/await, a simplified and elegant way to handle asynchronous operations in code.
It also provides some missing tools to work with strings and arrays, like a way to iterate through the properties of an Object, or dealing with pad spaces in strings. These tasks were provided in the past by polyfills functions or custom made implementations we kept in some “utils” directory, but now they are instead part of the language and are available for us to just use them.
Let’s review what are the most important changes made in this revision and what cool features it has to offer!.
Object.values() and Object.entries()
For a long time, this was a missing feature in the JS programmer’s tool-belt. These couple of methods are roughly equivalent for what in other OOP languages like C# or Java is referred as Iterators, in the special case when we work with object properties.
Before ES8, we could access the properties of an object by using Property Accessors, for example obj[‘propA’]
or obj.propA
to access propA
of Object obj
, but this requires a prior knowledge of what the obj properties are, this is OK for simple objects, but what happens with objects that contains multiple properties? and more important, what happens if these properties change?. This leads to constant errors injection in our code, as we are not providing code encapsulation.
Instead, using an iterator, we just walk though the properties, without any knowledge of what is inside, enforcing code encapsulation in an OOP fashion and providing a convenient way to access all properties in a single statement.
Object.values()
Returns an array of an object’s own enumerable property values.
As we saw earlier, before ES8, we needed to iterate through object properties using accessors, in some cases breaking code encapsulation and in others, writing several lines, one to access each property, increasing the amount of code needed.
With this new function, we can access every property with a single call, like in the following example.
Example
const obj = { foo: 'bar', abc: 42 }; console.log(Object.values(obj)); // ['bar', 43]
Object.entries()
This method returns an array of an object’s own enumerable properties as [key, value] pairs.
This is similar to Object.values, but this time we have access to both, the key (or name) and the value of every property for a particular object.
Example
const obj = { foo: 'bar', abc: 42 }; console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]
Notice that we get an array with sub-arrays in it, as the result of calling this function. Each sub-array has two elements, key and value. If you are interested only in the keys, you might proceed with a map call, like in this example,
const obj = { foo: 'bar', abc: 42 }; console.log(Object.entries(obj).map(([key]) => key)); // ['foo', 'abc']
String.prototype.padEnd()
A useful method when working with strings that we need often in Javascript, before ES8, we had to implement our own version or import some polyfills code in our project.
This method adds blank spaces into a string to fill up to a specified length.
str.padEnd(targetLength [, padString])
The improvement here is having an standardized implementation available to use, bug free and we now is going work the same in the browsers that supports it.
Example
const srt = 'qwerty'; str.padEnd(10) // "querty " str.padEnd(10, "foo"); // "quertyfoof"
String.prototype.padStart()
Similar to padEnd(), this method adds blank spaces to fill the length specified in the parameters for a given string.
str.padStart(targetLength [, padString])
Again, the advantage using this function, as with padEnd is having a native implementation, within the language.
Example
const srt = 'qwerty'; str.padStart(10); // " qwerty" str.padStart(10, "foo"); // "foofqwerty"
Object.getOwnPropertyDescriptors()
A method that returns all own property descriptors, enumerable or not, for a given object.
Object.getOwnPropertyDescriptors(obj)
Where obj
is the object to get all own property descriptors.
This method allows an examination of the description of an object’s own properties, like data descriptors or accessor descriptors (configurable, enumerable, non-enumerable, writable, etc) in the form of string-valued name names and property descriptor. For a detailed explanation of property descriptor take a look at Mozilla developers docs about Object.defineProperty()
Example
Before having this function in ES8, the way to create a shallow copy was using Object.assign
. But this function only swallows behaviors, accessing properties and symbols and not their descriptor, so we miss some possible accessors.
We can now create a shallow copy between two unknown objects by:
Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Async Functions
Perhaps the most significant addition in this version: Async Functions introduce a simplified and cleaner way to work with promises.
While the other features explained in the previously, came to solve some common problems in the day to day work with JavaScript. Async Functions present a new simplified and cleaner mean to work with Promises and Generators. In others words, is going to change the way we handle asynchronous operations in JavaScript.
Its syntax resembles the way asynchronous operations are made in other OOP languages like async/await
in C#.
async function
To understand async in ES7, let’s go deeper in seeing what a transpiler like babeljs does when it finds an async function and transpiles into regular ES6 code. For example, let’s consider the following trivial async function,
async function add(a, b) { return a + b; }
When babeljs transpiles this into ES6, we get,
let add = (() => { var _ref = _asyncToGenerator(function*(a, b) { return a + b; }); return function add(_x, _x2) { return _ref.apply(this, arguments); }; })(); function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step("next", value); }, function(err) { step("throw", err); } ); } } return step("next"); }); }; }
The first statement is an IIFE that returns the definition of function add
. This function, in turn, calls another function generated by babel called _asyncToGenerator
. As its name suggests, it works as a function generator proxy for the actual generator passed as fn
(defined in add
's body), and returns a Promise that calls the next()
function of fn
making use of accessor descriptor as key set to "next" literal.
As you can see, the result of calling "async add" is a promise, that once resolved can be handled with a proper callback function,
add(1,1).then(v => { console.log(v): })
async function expression
Very similar to async function
. The only difference is in this case we don't use function's name to get an anonymous function. This is useful to write asynchronous functions as arrow functions or as IIFE.
AsyncFunction
An object that represents an async function (as you might know, in JavaScript everything is an object, including functions).
Let’s examine what a AsyncFunction looks like again in babeljs transpiler, consider the follow line in ES8 syntax:
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
This line transpiles into ES6 as follows,
function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step("next", value); }, function(err) { step("throw", err); } ); } } return step("next"); }); }; } var AsyncFunction = Object.getPrototypeOf(_asyncToGenerator(function*() {})) .constructor;
As we could see in the previous example, async function
is translated to a function generator that returns Promises. So, in this case, AsyncFunction
is the constructor function that let us obtain this function generator and calls it to get a Promise we can handle when is resolved.
*Note that using AsyncFunction as we incur in the use of Object.getPrototypeOf to get the function generator, adding an extra expensive step.
await
As we could see, async function
is, in reality, a function generator hidden from us by the JS engine to make our lives easier. Nevertheless, is important to understand how it works underneath, so we can understand other syntaxis elements better when we use them in code.
In the case of await
, is just a way to yield the function generator execution. Let's consider another example using babeljs. This time suppose we have an add function that takes 2 seconds to complete, let's name it addAfter2Seconds
and then we have an async function
"add", that awaits for the result of the addition and logs the result,
function addAfter2Seconds(a,b) { return new Promise(resolve => { setTimeout(() => { resolve(a+b); }, 2000); }); } async function add(a,b) { var result = await addAfter2Seconds(1,1); console.log(result); }
Now let’s see how this translates in ES6,
let add = (() => { var _ref = _asyncToGenerator(function*(a, b) { var result = yield addAfter2Seconds(1, 1); console.log(result); }); return function add(_x, _x2) { return _ref.apply(this, arguments); }; })(); function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then( function(value) { step("next", value); }, function(err) { step("throw", err); } ); } } return step("next"); }); }; } function addAfter2Seconds(a, b) { return new Promise(resolve => { setTimeout(() => { resolve(a + b); }, 2000); }); }
As you can see, the only thing that changes from previous examples is the yield
operator in the function generator, that sets done property in the generator to true until addAfter2Seconds
it completes 2 seconds after. (See the function* docs on mozilla developer's site for more details).
That’s all for this overview on the new features of ES 2017. Thank you for reading this blog and please stay tuned for more exciting news and full-stack topics. Take care!.
Originally published at fullstackengine.net on December 15, 2017.