Everything you need to know from ES2016 to ES2019

Everything you need to know from ES2016 to ES2019

JavaScript is a language in constant evolution and in the past few years many new features have been added to the ECMAScript specification.

This article is an extract of my book Complete Guide to Modern JavaScript and it covers the new additions of ES2016, ES2017, ES2018, ES2019.

At the end of the article you will find a link to download a cheatsheet that summarizes everything.


Everything new in ES2016

ES2016 introduced only two new features:

  • Array.prototype.includes()
  • The exponential operator

Array.prototype.includes()

The includes() method will return true if our array includes a certain element, or false if it doesn't.

let array = [1,2,4,5];

array.includes(2);
// true
array.includes(3);
// false

Combine includes() with fromIndex

We can provide .includes() with an index to begin searching for an element. Default is 0, but we can also pass a negative value.

The first value we pass in is the element to search and the second one is the index:

let array = [1,3,5,7,9,11];

array.includes(3,1);
// find the number 3 starting from array index 1
// true
array.includes(5,4);
//false
array.includes(1,-1);
// find the number 1 starting from the ending of the array going backwards
// false
array.includes(11,-3);
// true

array.includes(5,4); returned false because, despite the array actually containing the number 5, it is found at the index 2 but we started looking at position 4. That's why we couldn't find it and it returned false.

array.includes(1,-1); returned false because we started looking at the index -1 (which is the last element of the array) and then continued from that point onward.

array.includes(11,-3); returned true because we went back to the index -3 and moved up, finding the value 11 on our path.


The exponential operator

Prior to ES2016 we would have done the following:

Math.pow(2,2);
// 4
Math.pow(2,3);
// 8

Now with the new exponential operator, we can do the following:

2**2;
// 4
2**3;
// 8

It will get pretty useful when combining multiple operations like in this example:

2**2**2;
// 16
Math.pow(Math.pow(2,2),2);
// 16

Using Math.pow() you need to continuously concatenate them and it can get pretty long and messy. The exponential operator provides a faster and cleaner way of doing the same thing.


educative banner

ES2017 string padding, Object.entries(), Object.values() and more

ES2017 introduced many cool new features, which we are going to see here.

String padding (.padStart() and .padEnd())

We can now add some padding to our strings, either at the end (.padEnd()) or at the beginning (.padStart()) of them.

"hello".padStart(6);
// " hello"
"hello".padEnd(6);
// "hello "

We specified that we want 6 as our padding, so then why in both cases did we only get 1 space? It happened because padStart and padEnd will go and fill the empty spaces. In our example "hello" is 5 letters, and our padding is 6, which leaves only 1 empty space.

Look at this example:

"hi".padStart(10);
// 10 - 2 = 8 empty spaces
// "        hi"
"welcome".padStart(10);
// 10 - 6 = 4 empty spaces
// "   welcome"

Right align with padStart

We can use padStart if we want to right align something.

const strings = ["short", "medium length", "very long string"];

const longestString = strings.sort(str => str.length).map(str => str.length)[0];

strings.forEach(str => console.log(str.padStart(longestString)));

// very long string
//    medium length
//            short

First we grabbed the longest of our strings and measured its length. We then applied a padStart to all the strings based on the length of the longest so that we now have all of them perfectly aligned to the right.


Add a custom value to the padding

We are not bound to just add a white space as a padding, we can pass both strings and numbers.

"hello".padEnd(13," Alberto");
// "hello Alberto"
"1".padStart(3,0);
// "001"
"99".padStart(3,0);
// "099"

Object.entries() and Object.values()

Let's first create an Object.

const family = {
  father: "Jonathan Kent",
  mother: "Martha Kent",
  son: "Clark Kent",
}

In previous versions of JavaScript we would have accessed the values inside the object like this:

Object.keys(family);
// ["father", "mother", "son"]
family.father;
"Jonathan Kent"

Object.keys() returned only the keys of the object that we then had to use to access the values.

We now have two more ways of accessing our objects:

Object.values(family);
// ["Jonathan Kent", "Martha Kent", "Clark Kent"]

Object.entries(family);
// ["father", "Jonathan Kent"]
// ["mother", "Martha Kent"]
// ["son", "Clark Kent"]

Object.values() returns an array of all the values whilst Object.entries() returns an array of arrays containing both keys and values.


Object.getOwnPropertyDescriptors()

This method will return all the own property descriptors of an object. The attributes it can return are value, writable, get, set, configurable and enumerable.

const myObj = {
  name: "Alberto",
  age: 25,
  greet() {
    console.log("hello");
  },
}
Object.getOwnPropertyDescriptors(myObj);
// age:{value: 25, writable: true, enumerable: true, configurable: true}

// greet:{value: ƒ, writable: true, enumerable: true, configurable: true}

// name:{value: "Alberto", writable: true, enumerable: true, configurable: true}

Trailing commas in function parameter lists and calls

This is just a minor change to a syntax. Now, when writing objects we can leave a trailing comma after each parameter, whether or not it is the last one.

// from this
const object = {
  prop1: "prop",
  prop2: "propop"
}

// to this
const object = {
  prop1: "prop",
  prop2: "propop",
}

Notice how I wrote a comma at the end of the second property. It will not throw any error if you don't put it, but it's a better practice to follow as it make your colleague’s or team member’s life easier.

// I write
const object = {
  prop1: "prop",
  prop2: "propop"
}

// my colleague updates the code, adding a new property
const object = {
  prop1: "prop",
  prop2: "propop"
  prop3: "propopop"
}
// suddenly, he gets an error because he did not notice that I forgot to leave a comma at the end of the last parameter.

Shared memory and Atomics

From MDN:

When memory is shared, multiple threads can read and write the same data in memory. Atomic operations make sure that predictable values are written and read, that operations are finished before the next operation starts and that operations are not interrupted.

Atomics is not a constructor, all of its properties and methods are static (just like Math) therefore we cannot use it with a new operator or invoke the Atomics object as a function.

Examples of its methods are:

  • add / sub
  • and / or / xor
  • load / store

Atomics are used with SharedArrayBuffer (generic fixed-length binary data buffer) objects which represent generic, fixed-length raw binary data buffer.

Let's have a look at some examples of Atomics methods:

Atomics.add(), Atomics.sub(), Atomics.load() and Atomics.store()

Atomics.add() will take three arguments, an array, an index and a value and will return the previous value at that index before performing an addition.

// create a `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// add a value at the first position
uint8[0] = 10;

console.log(Atomics.add(uint8, 0, 5));
// 10

// 10 + 5 = 15
console.log(uint8[0])
// 15
console.log(Atomics.load(uint8,0));
// 15

As you can see, calling Atomics.add() will return the previous value at the array position we are targeting. when we call again uint8[0] we see that the addition was performed and we got 15.

To retrieve a specific value from our array we can use Atomics.load and pass two argument, an array and an index.

Atomics.sub() works the same way as Atomics.add() but it will subtract a value.

// create a `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// add a value at the first position
uint8[0] = 10;

console.log(Atomics.sub(uint8, 0, 5));
// 10

// 10 - 5 = 5
console.log(uint8[0])
// 5
console.log(Atomics.store(uint8,0,3));
// 3
console.log(Atomics.load(uint8,0));
// 3

Here we are using Atomics.sub() to substract 5 from the value at position uint8[0] which is equivalent to 10 - 5. Same as with Atomics.add(), the method will return the previous value at that index, in this case 10.

We are then using Atomics.store() to store a specific value, in this case 3, at a specific index of the array, in this case 0, the first position. Atomics.store() will return the value that we just passed, in this case 3. You can see that when we call Atomics.load() on that specific index we get 3 and not 5 anymore.


Atomics.and(), Atomics.or() and Atomics.xor()

These three methods all perform bitwise AND, OR and XOR operations at a given position of the array. You can read more about bitwise operations on Wikipedia at this link https://en.wikipedia.org/wiki/Bitwise_operation


ES2017 Async and Await

ES2017 introduced a new way of working with promises, called "async/await".


Promise review

Before we dive in this new syntax let's quickly review how we would usually write a promise:

// fetch a user from github
fetch('api.github.com/user/AlbertoMontalesi').then( res => {
  // return the data in json format
  return res.json();
}).then(res => {
  // if everything went well, print the data
  console.log(res);
}).catch( err => {
  // or print the error
  console.log(err);
})

This is a very simple promise to fetch a user from GitHub and print it to the console.

Let's see a different example:

function walk(amount) {
  return new Promise((resolve,reject) => {
    if (amount < 500) {
      reject ("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
  });
}

walk(1000).then(res => {
  console.log(res);
  return walk(500);
}).then(res => {
  console.log(res);
  return walk(700);
}).then(res => {
  console.log(res);
  return walk(800);
}).then(res => {
  console.log(res);
  return walk(100);
}).then(res => {
  console.log(res);
  return walk(400);
}).then(res => {
  console.log(res);
  return walk(600);
});

// you walked for 1000ms
// you walked for 500ms
// you walked for 700ms
// you walked for 800ms
// uncaught exception: the value is too small

Let's see how we can rewrite this Promise with the new async/await syntax.


Async and Await

function walk(amount) {
  return new Promise((resolve,reject) => {
    if (amount < 500) {
      reject ("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
  });
}

// create an async function
async function go() {
  // use the keyword `await` to wait for the response
  const res = await walk(500);
  console.log(res);
  const res2 = await walk(900);
  console.log(res2);
  const res3 = await walk(600);
  console.log(res3);
  const res4 = await walk(700);
  console.log(res4);
  const res5 = await walk(400);
  console.log(res5);
  console.log("finished");
}

go();

// you walked for 500ms 
// you walked for 900ms 
// you walked for 600ms 
// you walked for 700ms 
// uncaught exception: the value is too small

Let's break down what we just did:

  • to create an async function we need to put the async keyword in front of it
  • the keyword will tell JavaScript to always return a promise
  • if we specify to return <non-promise> it will return a value wrapped inside a promise
  • the await keyword only works inside an async function
  • as the name implies, await will tell JavaScript to wait until the promise returns its result

Let's see what happens if we try to use await outside an async function

// use await inside a normal function
function func() {
  let promise = Promise.resolve(1);
  let result = await promise; 
}
func();
// SyntaxError: await is only valid in async functions and async generators


// use await in the top-level code
let response = Promise.resolve("hi");
let result = await response;
// SyntaxError: await is only valid in async functions and async generators

Remember: You can only use await inside an async function.


Error handling

In a normal promise we would use .catch() to catch eventual errors returned by the promise. Here, it is not much different:

async function asyncFunc() {

  try {
    let response = await fetch('http:your-url');
    } catch(err) {
        console.log(err);
      }
}

asyncFunc();
// TypeError: failed to fetch

We use try...catch to grab the error, but in a case where we do not have them we can still catch the error like this:

async function asyncFunc(){
  let response = await fetch('http:your-url');
}
asyncFunc();
// Uncaught (in promise) TypeError: Failed to fetch

asyncFunc().catch(console.log);
// TypeError: Failed to fetch

educative banner

ES2018 Async Iteration and more?

Let's now have a look at what was introduced with ES2018.


Rest / Spread for Objects

Remember how ES6 (ES2015) allowed us to do this?

const veggie = ["tomato","cucumber","beans"];
const meat = ["pork","beef","chicken"];

const menu = [...veggie, "pasta", ...meat];
console.log(menu);
// Array [ "tomato", "cucumber", "beans", "pasta", "pork", "beef", "chicken" ]

Now we can use the rest/spread syntax for objects too, let's look at how:

let myObj = {
  a:1,
  b:3,
  c:5,
  d:8,
}

// we use the rest operator to grab everything else left in the object.
let { a, b, ...z } = myObj;
console.log(a);     // 1
console.log(b);     // 3
console.log(z);     // {c: 5, d: 8}

// using the spread syntax we cloned our Object
let clone = { ...myObj };
console.log(clone);
// {a: 1, b: 3, c: 5, d: 8}
myObj.e = 15;
console.log(clone)
// {a: 1, b: 3, c: 5, d: 8}
console.log(myObj)
// {a: 1, b: 3, c: 5, d: 8, e: 15}

With the spread operator we can easily create a clone of our Object so that when we modify the original Object, the clone does not get modified, similarly to what we saw when we talked about arrays.


Asynchronous Iteration

With Asynchronous Iteration we can iterate asynchronously over our data.

From the documentation:

An async iterator is much like an iterator, except that its next() method returns a promise for a { value, done } pair.

To do so, we will use a for-await-of loop which works by converting our iterables to a Promise, unless they already are one.

const iterables = [1,2,3];

async function test() {
    for await (const value of iterables) {
        console.log(value);
    }
}
test();
// 1
// 2
// 3

During execution, an async iterator is created from the data source using the [Symbol.asyncIterator]() method. Each time we access the next value in the sequence, we implicitly await the promise returned from the iterator method.


Promise.prototype.finally()

After our promise has finished we can invoke a callback.

const myPromise = new Promise((resolve,reject) => {
  resolve();
})
myPromise
  .then( () => {
    console.log('still working');
  })
  .catch( () => {
    console.log('there was an error');
  })
  .finally(()=> {
    console.log('Done!');
  })

.finally() will also return a Promise so we can chain more then and catch after it but those Promises will fulfill based on the Promise they were chained onto.

const myPromise = new Promise((resolve,reject) => {
  resolve();
})
myPromise
.then( () => {
    console.log('still working');
    return 'still working';
  })
  .finally(()=> {
    console.log('Done!');
    return 'Done!';
  })
  .then( res => {
    console.log(res);
  })
// still working
// Done!
// still working

As you can see the then chained after finally returned the value that was returned by the Promise created not by finally but by the first then.


RegExp features

Four new RegExp related features made it to the new version of ECMAScript. They are:


s (dotAll) flag for regular expression

This introduces a new s flag for ECMAScript regular expressions that makes . match any character, including line terminators.

/foo.bar/s.test('foo\nbar');
// true

RegExp named capture groups

From the documentation:

Numbered capture groups allow one to refer to certain portions of a string that a regular expression matches. Each capture group is assigned a unique number and can be referenced using that number, but this can make a regular expression hard to grasp and refactor. For example, given /(\d{4})-(\d{2})-(\d{2})/ that matches a date, one cannot be sure which group corresponds to the month and which one is the day without examining the surrounding code. Also, if one wants to swap the order of the month and the day, the group references should also be updated. A capture group can be given a name using the (?<name>...) syntax, for any identifier name. The regular expression for a date then can be written as /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u. Each name should be unique and follow the grammar for ECMAScript IdentifierName.Named groups can be accessed from properties of a groups property of the regular expression result. Numbered references to the groups are also created, just as for non-named groups. For example:

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
console.log(`one: ${one}, two: ${two}`); 
// one: foo, two: bar

RegExp Lookbehind Assertions

From the documentation:

With lookbehind assertions, one can make sure that a pattern is or isn't preceded by another, e.g. matching a dollar amount without capturing the dollar sign. Positive lookbehind assertions are denoted as (?<=...) and they ensure that the pattern contained within precedes the pattern following the assertion. For example, if one wants to match a dollar amount without capturing the dollar sign, /(?<=$)\d+(\.\d*)?/ can be used, matching '$10.53' and returning '10.53'. This, however, wouldn't match €10.53. Negative lookbehind assertions are denoted as (?<!...) and, on the other hand, make sure that the pattern within doesn't precede the pattern following the assertion. For example, /(?<!$)\d+(?:\.\d*)/ wouldn't match '$10.53', but would '€10.53'.


RegExp Unicode Property Escapes

From the documentation:

This brings the addition of Unicode property escapes of the form \p{…} and \P{…}. Unicode property escapes are a new type of escape sequence available in regular expressions that have the u flag set. With this feature, we could write:

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π');
// true

Lifting template literals restriction

When using tagged template literals the restriction on escape sequences are removed.

You can read more here.


educative banner

What's new in ES2019?

Let's have a look at what is included in the latest version of ECMAScript: ES2019.


Array.prototype.flat() / Array.prototype.flatMap()

Array.prototype.flat() will flatten the array recursively up to the depth that we specify. If no depth argument is specified, 1 is the default value. We can use Infinity to flatten all nested arrays.

const letters = ['a', 'b', ['c', 'd', ['e', 'f']]];
// default depth of 1
letters.flat();
// ['a', 'b', 'c', 'd', ['e', 'f']]

// depth of 2
letters.flat(2);
// ['a', 'b', 'c', 'd', 'e', 'f']

// which is the same as executing flat with depth of 1 twice
letters.flat().flat();
// ['a', 'b', 'c', 'd', 'e', 'f']

// Flattens recursively until the array contains no nested arrays
letters.flat(Infinity)
// ['a', 'b', 'c', 'd', 'e', 'f']

Array.prototype.flatMap() is identical to the previous one with regards to the way it handles the 'depth' argument but instead of simply flattening an array, with flatMap() we can also map over it and return the result in the new array.

let greeting = ["Greetings from", " ", "Vietnam"];

// let's first try using a normal `map()` function
greeting.map(x => x.split(" "));
// ["Greetings", "from"]
// ["", ""]
// ["Vietnam"]


greeting.flatMap(x => x.split(" "))
// ["Greetings", "from", "", "", "Vietnam"]

As you can see, if we use .map() we will get a multi level array, a problem that we can solve by using .flatMap() which will also flatten our array.


Object.fromEntries()

Object.fromEntries() transforms a list of key-value pairs into an object.

const keyValueArray = [
  ['key1', 'value1'],
  ['key2', 'value2']
]

const obj = Object.fromEntries(keyValueArray)
// {key1: "value1", key2: "value2"}

We can pass any iterable as argument of Object.fromEntries(), whether it's an Array, a Map or other objects implementing the iterable protocol.

You can read more about the iterable protocol here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol


String.prototype.trimStart() / .trimEnd()

String.prototype.trimStart() removes white space from the beginning of a string while String.prototype.trimEnd() removes them from the end.

let str = "    this string has a lot of whitespace   ";

str.length;
// 42

str = str.trimStart();
// "this string has a lot of whitespace   "
str.length;
// 38

str = str.trimEnd();
// "this string has a lot of whitespace"
str.length;
// 35

We can also use .trimLeft() as an alias of .trimStart() and .trimRight() as an alias of .trimEnd().


Optional Catch Binding

Prior to ES2019 you had to always include an exception variable in your catch clause. E2019 allows you to omit it.

// Before
try {
   ...
} catch(error) {
   ...
}

// ES2019
try {
   ...
} catch {
   ...
}

This is useful when you want to ignore the error. For a more detailed list of use cases for this I highly recommend this article: http://2ality.com/2017/08/optional-catch-binding.html


Function​.prototype​.toString()

The .toString() method returns a string representing the source code of the function.

function sum(a, b) {
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//    return a + b;
//  }

It also includes comments.

function sum(a, b) {
  // perform a sum
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//   // perform a sum
//   return a + b;
// }

Symbol.prototype.description

.description returns the optional description of a Symbol Object.

const me = Symbol("Alberto");
me.description;
// "Alberto"

me.toString()
//  "Symbol(Alberto)"

Download the cheatsheet

If you want to download this cheatsheet please follow this link



Thank you very much for reading, if you enjoyed this article, please share it with friends and colleagues and if there is a topic you would like me to cover, reach out to me on twitter at @montalesi. Follow me on DevTo or on Twitter for more.

complete guide to modern javascript alberto montalesi ebook bannerGet my ebook on Amazon and Leanpub or get my course on Educative


NEWEST ARTICLES




ABOUT ME

author alberto montalesi profile picture

Alberto is a software developer specialized in building enterpise software using Angular and author of the 'Complete guide to Modern JavaScript' ebook and course. In his free time he writes articles and tutorials on InspiredWebDev.com and Dev.to

You can read more about him here