ES2018: A review of the new features with code examples
Last updated: January 2, 2022.
Following the major ES6 upgrade to the JavaScript language in 2015, new features have been added on a rolling annual basis.
In this article, we take a look at the new features introduced to JavaScript by ES2018 with code examples.
Extending spreading and getting the ‘rest’ to objects
ES6 introduced the rest and spread operator (...)
for arrays.
With this, you can ‘spread’ the contents of an array (or several arrays) into something:
const array1 = ["a", "b", "c"]; const array2 = ["e", "f", "g"]; // Spread contents of arrays into new array const mergedArray = [...array1, ...array2]; console.log(mergedArray); // ['a', 'b', 'c', 'e', 'f', 'g']
Also using the spread operator, you can get the ‘rest’ of an array that is useful when destructing arrays:
const mergedArray = ['a', 'b', 'c', 'e', 'f', 'g'] const [firstElement, secondElement, ...rest] = mergedArray; console.log(firstElement); // a console.log(secondElement); // b console.log(rest); // ['c', 'e', 'f', 'g']
ES2018 introduces extends this functionality like-for-like to objects as well.
For example, below we create a new object (mergedObj
) by spreading the contents of two existing objects:
const obj1 = { firstName: "Tatty", lastName: "Bogle", } const obj2 = { birthplace: "London", dob: "09/03/1970", } const mergedObj = {...obj1,...obj2} console.log(mergedObj); // {firstName: 'Tatty', lastName: 'Bogle', birthplace: 'London', dob: '09/03/1970'}

But watch out: if you spread multiple objects with some identical keys into a new object, the properties of the last object inserted will overwrite any earlier ones:
const user1 = { firstName: "Tatty", lastName: "Bogle", } const user2 = { firstName: "Wizzy", lastName: "Dora", } const allUsers = {...user1,...user2}; console.log(allUsers); // {firstName: 'Wizzy', lastName: 'Dora'}
In this use case, you probably want to create an array of objects:
const user1 = { firstName: "Tatty", lastName: "Bogle", } const user2 = { firstName: "Wizzy", lastName: "Dora", } const allUsers = [{...user1},{...user2}]; console.log(allUsers); // [ // { // "firstName": "Tatty", // "lastName": "Bogle" // }, // { // "firstName": "Wizzy", // "lastName": "Dora" // } // ]
ES2018 now allows the spread operator to be used in object destructuring. In the following example, we destructure an object, saving its firstName
and lastName
properties to variables and storing the remaining properties in an object saved in a variable called rest
:
const obj = { firstName: "Tatty", lastName: "Bogle", birthplace: "London", dob: "09/03/1970", }; const {firstName, lastName, ...rest} = obj; console.log(firstName); // Tatty console.log(lastName); // Bogle console.log(rest); // {birthplace: 'London', dob: '09/03/1970'}
Using .finally() with promises
A promise can resolve successfully or be rejected. We can use .then() to execute code when a promise is successful and .catch() to run some code when it is not.
.finally() introduces the ability to run some code when a promise returns a result, regardless of the outcome of the result.
For example, in the below code we define a fetch function (fetchSomething) that when called, will return a successfully resolved promise after two seconds.
We make the fetch request within the handleFetch
function and use .then and .catch to process the result. We attach .finally() to this chain. Unlike .then and .catch, which are run conditional upon the outcome, .finally() will always run.
// Simulate successful fetch request (takes two seconds) function fetchSomething() { return new Promise ((resolve) => {setTimeout(() => {resolve("Data");},2000)}) } // Function runs retch request and logs result function handleFetch() { fetchSomething() .then(res => console.log(res)) .catch(err => console.log(err)) .finally(() => console.log("Finished processing")) } // Runs handleFetch handleFetch(); // Console log (after two seconds): // Data // Finished processing
Now we replace the fetchSomething function with the following code, which returns a rejected promise after two seconds:
// Simulate successful fetch request (takes two seconds) function fetchSomething() { return new Promise ((resolve, reject) => {setTimeout(() => {reject("Error");},2000)}) } // Function runs retch request and logs result function handleFetch() { fetchSomething() .then(res => console.log(res)) .catch(err => console.log(err)) .finally(() => console.log("Finished processing")) } // Runs handleFetch handleFetch(); // Console log (after two seconds): // Error // Finished processing
Asynchronous iteration
ES2017 introduced the new async/await syntax to JavaScript. This syntax enables the writing of asynchronous JavaScript functions that looks just like regular, synchronous but with with the keywords async
and await
added.
ES2018 extends this to iteration so that it is now possible to wait upon the outcome of many asynchronous operations using the for...of loop
.
First, let create a simulated fetch request that will successfully resolve in two seconds.
// Simulate fetch method (returns a promise) function fetchSomething() { return new Promise ((resolve) => {setTimeout(() => {resolve("Data");},2000)}) }
Now, if we create an array containing multiple instances of calling the fetchSomething
function and try to iterate through it to get the results using a normal for...of
loop, all we will get is pending promises.
// Run fetch function to get data function getData() { const promises = [ fetchSomething(), fetchSomething(), fetchSomething() ] // Normal loop for (const promiseResult of promises) { console.log(promiseResult) // immediately returns 3x Promise {<pending>} } } getData()
But if we make getData
an async function and use the keyword await
after the for
opening the loop (so we are waiting upon its result now) we get a result:
// Run fetch function to get data async function getData() { const promises = [ fetchSomething(), fetchSomething(), fetchSomething() ] // Loops for await (const promiseResult of promises) { console.log(promiseResult) // after two seconds, returns 3x"Data" } } getData()
Group names for regular expression matches
Regular expressions are used to filter out parts of a string by searching for 'matches' between a string and the parameters of the regular expression:
const // Create expression regExp = /([0-9]{2})-([0-9]{2})-([0-9]{4})/, // Execute regular expression on string StringToMatch = regExp.exec('02-12-2021'), year = StringToMatch[3], // 2021 month = StringToMatch[2], // 12 day = StringToMatch[1]; // 02
ES2018 introduces named groups that can be added inside of a regular expression using the ?<> syntax. This group names can then be referenced when finding matches:
const regExp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/ const StringToMatch = regExp.exec('2021-12-23') StringToMatch.groups.year; // 2021 StringToMatch.groups.month; // 12 StringToMatch.groups.day; // 12
Changing regular expression dot (.) behaviour with 's' flag
In regular expressions, a dot represents acts like a wildcard, representing any character. However, before ES8, this did not include line returns (/n
);
/amazing.javascript/.test('amazing\njavascript'); // false
To change this, ES8 introduces an 's' flag that can be added as the end of a regular expression capture. This makes dot also match a line return in a string:
/amazing.javascript/s.test('amazing\njavascript'); // true
Unicode in regular expressions
ES2018 now allows Unicode characters to be matched by regular expressions.
Unicode is represented in a regular expression by the p{}
syntax. The type of Unicode is specified inside the curly braces.
Note that the Unicode 'u' flag must be set for this to work. Otherwise, any match attempted will return false
(see final example):
// RegExp testing for any white space const expression = /\p{White_Space}/u; console.log(expression.test(' ')); // true console.log(expression.test('...')); // false // Testing if string contains a letter const anotherExpression = /\p{Letter}/u; console.log(anotherExpression.test('a')); // true console.log(anotherExpression.test('1')); // false // Testing if string contains a greek symbol const oneMoreExpression = /\p{Script=Greek}/u; console.log(oneMoreExpression.test('π')); // true console.log(oneMoreExpression.test('PI')); // false // Without 'u' flag, match will always return false const noFlagExpression = /\p{White_Space}/; console.log(noFlagExpression.test(' ')); // false console.log(noFlagExpression.test('...')); // false
Lookbehind in regular expressions
ES2018 now permits regular expressions to contain lookbehind assertions (previously, only lookahead assertions were valid).
A lookbehind assertion looks at the string content before some searched for string content.
The syntax to add a lookbehind is (?<=content)
with content
replaced by whatever you are looking behind for.
A very common use case is looking for a currency symbol before a number. To check for a dollar sign before an unspecified number of characters (d+), the sytnax would be /(?<=\$)\d+/
.
In the below code snippet we use a lookbehind assertion to replace a dollar with a euro symbol. This is a positive lookbehind assertion because it looks back to see if come string content exists ($).
// Create a value to check const value = "$30" // If a dollar sign exists before some characters... const DollarExpression = /(?<=$)\d+/; // ...replace $ with € console.log(value.replace(/[$]/, "€")); // €30
A negative lookbehind assertion is also possible. This looks for the non-existence of some string content (here again, a dollar symbol) before some content. The syntax for a negative lookback assertion almost the same as for a positive lookback assertion except = is replaced by !
:
// Creat a value to check const value2 = "30" // If no dollar exists before a string... const NoDollarExpression = /(?<!$)\d+/; // ...add a dollar console.log(value2.replace(/(?<!$)/, '$')); // $30
Lifting of illegal escapes on tagged template literals
ES6 introduced template literals than can be tagged by a function.
At the time of their introduction, illegal escapes were not allowed either in the string literal itself or the tagging function. For example:
"\u
" - for a unicode escape
"\x
" - for a unicode escape
"\0o
"- for a unicode escape
The first two are particularly problematic because they disallow the writing of filepaths:
function tagged(str) { str[0] } tagged`\universe` // undefined tagged`\xfiles` // undefined
Now this is possible within the tagging function by accessing the .raw
property of the string literal. The .raw property is the new ES8 feature to allow examples like the above to be used in tagged template:
function tagged(str) { document.getElementById('output').append(str.raw) } tagged`\universe` // \universe tagged`\xfiles` // \xfiles
It is important to note, however, that restrictions on these escapes are only lifted in tagged template literals.
These escapes are still illegal in template literals themselves:
const x = `\universe` console.log(x) // Uncaught SyntaxError: Invalid Unicode escape sequence