- Published on
Everything you need to know from ES2016 to ES2019
- Authors
- Name
- Alberto Montalesi
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
includes()
with fromIndex
Combine 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.
Object.entries()
, Object.values()
and more
ES2017 string padding, ES2017 introduced many cool new features, which we are going to see here.
.padStart()
and .padEnd()
)
String padding (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"
padStart
Right align with 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.
Atomics
Shared memory and 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 theasync
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 anasync
function - as the name implies,
await
will tellJavaScript
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 anasync
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
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 expressions- RegExp named capture groups
- RegExp Lookbehind Assertions
- RegExp Unicode Property Escapes
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
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 identifiername
. 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 forECMAScript
IdentifierName.Named groups can be accessed from properties of agroups
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
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
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 theu
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.
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