Looking back at the ES2017 updates to JavaScript

OpenJavaScript 0
Reading Time: 5 minutes 🕑

Last updated: January 2, 2022.

Since the major ES6 upgrade to the JavaScript language, new features have been added on a rolling annual basis.

ES2017 introduced a significant number of new features. In this article, we review each of these with code use examples.

Asnyc/await

Perhaps the most significant new feature that ES2017 introduces is the async/await syntax.

This syntax allows the writing of asynchronous code that looks very similar to synchronous code.

To demonstrate, let’s create a function that simulates a fetch request and try handing its outcome using a standard function:

// Simulate fetch request (takes two seconds)
function fetchSomething() {
    return new Promise ((resolve) => {setTimeout(() => {resolve("Data");},2000)})
}

// Function runs retch request and logs result
function handleFetch() {
    const result = fetchSomething();
    console.log(result);
}

// Runs handleFetch
handleFetch(); // Promise {<pending>}

It doesn't work because the simulated fetch request takes two seconds but we try processing the result immediately. What we need to do is wait for the result of the fetch request before processing its result.

Async/await makes this easy: we preprend the keyword async to the handleFetch function and place the keyword await in front of the function call to run the fetch request:

// Simulate fetch request
function fetchSomething() {
    return new Promise ((resolve) => {setTimeout(() => {resolve("Data");},2000)})
}

// Function runs retch request and logs result
async function handleFetch() {
    const result = await fetchSomething();
    console.log(result);
}

// Runs handleFetch
handleFetch(); // Data (after 2 seconds)

You can use the keyword await in front of anything that returns a promise.

Note that you must place async before the function definition, otherwise await will not work inside the function!

Error-handling

Not all promises we wait upon resolve successfully. Async/await can handle errors using the try catch syntax.

To demonstrate, we change our simulated fetch request to reject after two seconds.

To deal with this inside our async/await function, we wrap our previous code inside try{}. This is the code that will be executed if the process we await executes successfully.

Following this, we specify how we want to handle an error inside catch (err) {}. Notice that we have the error available to us as a parameter. This contains information about the error. We use the keyword throw followed by the error so JavaScript stops executing any further code and logs this an error in the console.

// Simulate fetch request
function fetchSomething() {
    return new Promise ((resolve, reject) => {setTimeout(() => {reject("Fetch failed");},2000)})
}
 
// Function runs fetch request and logs result
async function handleFetch() {
    try {
    const result = await fetchSomething();
    console.log(result);
    } catch (err) {
    console.log(err);
    throw err
    }
}

Async/await under the hood

Async/await is in fact not adding new functionality to the language. The processing of the request above could be acheived identically using promises instead:

// Simulate fetch request
function fetchSomething() {
    return new Promise ((resolve) => {setTimeout(() => {resolve("Data");},2000)})
}

// Function runs retch request and logs result
function handleFetch() {
    fetchSomething()
    .then(result => console.log(result));
}

// Runs handleFetch
handleFetch();

So why use async/await instead of promises?

The main advantage, as demonstrated in the first two examples, is that async/await allows us to handle asynchronous operations (i.e. handle promises) with code that is very similar to standard, synchronous code.

Object.values and Object.entries

ES5 (2009) already introduced the Object.keys() method. It accepts a JavaScript object and outputs its key values as an array:

const yearOfThe = {
    2019: "Pig",
    2020: "Rat",
    2021: "Ox",  
    2022: "Tiger"   
};

let output = Object.keys(yearOfThe);

console.log(output); // ['2019', '2020', '2021', '2022']

This is already insanely useful because you cannot loop through an object numerically. But you can if you query an object by its key values each in turn:

for (i=0; i<output.length; i++) {
    console.log(yearOfThe[output[i]]) 
    // (1) Pig
    // (2) Rat
    // (3) Ox
    // (4) Tiger
}

Object.values makes our lives easier by getting the values directly without the need for a loop and outputs these in array format:

let output = Object.values(yearOfThe);

console.log(output); // ['Pig', 'Rat', 'Ox', 'Tiger']

Object.entries outputs each key-value pair in an array and stores each of these in an array:

let output = Object.entries(yearOfThe);

console.log(output); 
// [["2019","Pig"],["2020","Rat"],["2021","Ox"],["2022","Tiger"]]

So now we can easily get the keys, values or key-value pairs of an object with a single method!

const yearOfThe = {
    2019: "Pig",
    2020: "Rat",
    2021: "Ox",  
    2022: "Tiger"   
};

// Already available in ES5 (2009)
Object.keys(yearOfThe); // ['2019', '2020', '2021', '2022']

// New in ES2017
Object.values(yearOfThe); // ['Pig', 'Rat', 'Ox', 'Tiger']
Object.entries(yearOfThe); // ['2019','Pig'],['2020','Rat'],['2021','Ox'],['2022','Tiger']]

Add padding to a string with .padStart() and .padEnd()

The .padStart() and .padEnd() were introduced as new methods available to use on strings. Both adding a specified amount of padding and, optionally, new content to an existing string.

Here is the sytnax of .padStart() and .padEnd()

string.padStart(new string length, content to add)

string.padEnd(new string length, content to add)

The first parameter is the total length of the new string with padding added (not the amount of padding to add). For example:

        let newString = "string"

        newString.padStart(10) // returns "    string" (length 10)
        
        newString.padEnd(10) // returns "string    " (length 10)

        // If first argument less than original string length, no effect
        newString.padStart(1) // returns "string" (length 6)

If new string content is added as a second parameter, it is inserted into the new padding added.

        let newString = "string"

        newString = newString.padStart(10, "new ")
        console.log(newString) // "new string"
        
        newString = newString.padEnd(newString.length+" content".length, " content")
        console.log(newString) // "new string content"

If the new string is longer or shorter than the new padding space, the new content is cut or repeated:

        let newString = "string"

        newString.padStart(20, "new ") // returns "new new new nestring"
        
        newString.padEnd(10, " content") // returns "string con"

When adding content, it is therefore best to calculate the new padding to be added dynamically based upon the (i) length of the existing string and (ii) the length of the new string content to add:

        let newString = "string"

        let contentToAdd = " with added string content"

        let newStringLength = newString.length + contentToAdd.length

        newString = newString.padEnd(newStringLength, contentToAdd)

        console.log(newString) // "string with added string content"

Trailing commas in functions parameters

ES2017 permits trailing commas to be added to the end of a function parameter list. For example, the follow syntax is valid:

function doSomething (p1,
p2,
) {
    p1+p2
}

The value of this has to do with version control: a new parameter can be added without changing the previous line.

function doSomething (p1,
p2,
p3,
) {
    p1+p2+p3
}

In this instance, a version control system will recognise that a new line has been added (line 3). But it will not detect a change in line 2 (which is useful because the second parameter has not changed).

Object.getOwnPropertyDescriptors()

The Object.getOwnPropertyDescriptors() method accepts an object and returns an object with descriptors for each property.

For a regular key-value pair the returned values are:

  • value - holds the value of the property
  • writable - can value of the property can be changed (returns true/false)
  • enumerable - can property be iterated over (returns true/false)
  • configurable - can change more than its value (returns true/false)

For a getter or setter function:

  • value - value of the property
  • writable - can value of the property can be changed (returns true/false)
  • get - getter function description (undefined if not a get function)
  • set - setter function description (undefined if not a set function)
const myObject = {  
    firstName: "Rob",
    lastName: "Smith",
    
    get fullName(){this.firstName+" "+this.lastName},

    set setFirstName(input) {this.firstName = input},
    set setSurname(input) {this.LastName = input}
}  

const descriptors = Object.getOwnPropertyDescriptors(myObject);  


console.log(descriptors);

// Copy of the returned object:
// {
//     "firstName": {
//         "value": "Rob",
//         "writable": true,
//         "enumerable": true,
//         "configurable": true
//     },
//     "lastName": {
//         "value": "Smith",
//         "writable": true,
//         "enumerable": true,
//         "configurable": true
//     },
//     "fullName": {
//         "enumerable": true,
//         "configurable": true
//     },
//     "setFirstName": {
//         "enumerable": true,
//         "configurable": true
//     },
//     "setSurname": {
//         "enumerable": true,
//         "configurable": true
//     }
// }

Note that the returned object omits the getter and setter functions because they do not have a 'real' value (length = 0). But their values are nevertheless visible in the console and can be called directly:

console.log(descriptors.setSurname.set);
// ƒ setSurname(input) {this.LastName = input}

console.log(descriptors.fullName.get);
// ƒ fullName(){this.firstName+" "+this.lastName}

Shared memory and the Atomics object

ES2017 introduces Atomics as a global variable. This is an advanced feature that can be used to help support multi-threaded code written in other languages (e.g. C and C++) that are translated to JavaScript. It is most useful for CPU-intensive apps such as games where memory assignment is a priority.

In case you want to learn more about this feature, Lars T Hansen, author of this proposal, has written a great tutorial on how to make use of it.

Summary

In this article, we have covered the new features introduced to the JavaScript language in the ES2017 update.

Want to know more about new additions to JavaScript? We cover everything you need to know in our ES6+ section.