ES2018: A review of the new features with code examples

OpenJavaScript 0
Reading Time: 6 minutes 🕑

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