5 mins read

Everything You Need to Know About ECMAScript 2018 (ES9)

Hey, JavaScript Lovers!

ECMAScript 2018, also known as ES2018 or ES9, is a version of the ECMAScript standard, which is the foundation for JavaScript. It was finalized in June 2018 and introduced several enhancements and new features to improve the language.

Some of the key features introduced in ES9 include:

  1. Asynchronous Iteration
  2. Rest/Spread Properties
  3. RegExp Enhancements
  4. Promise.prototype.finally
  5. JSON.stringify Improvements
  6. Function.prototype.toString
  7. Object.prototype.__proto__

1. Asynchronous Iteration

Asynchronous iteration, introduced in ECMAScript 2018 (ES9), simplifies handling asynchronous operations. Before ES9, managing asynchronous data often involved nested callbacks or Promises, which could complicate and clutter the code. With the addition of asynchronous iteration, you can use for-await-of loops to work with asynchronous data sources more intuitively and manageable.

Here’s a basic example of using for-await-of to iterate over an asynchronous data source:

async function* fetchNumbers() {
    // Simulates an asynchronous data source, like an API or database.
    yield new Promise(resolve => setTimeout(() => resolve(1), 1000));
    yield new Promise(resolve => setTimeout(() => resolve(2), 1000));
    yield new Promise(resolve => setTimeout(() => resolve(3), 1000));
}

async function processNumbers() {
    for await (const number of fetchNumbers()) {
        console.log(number); // Logs 1, then 2, then 3, with a delay in between.
    }
}

processNumbers();

Use Cases

Streaming Data:

Useful for handling data streams from APIs or other sources where you want to process data as it arrives.

Concurrent Asynchronous Operations:

Manage multiple asynchronous operations and process results one by one.

Improved Readability

It provides a more readable and maintainable way to work with asynchronous code compared to nested callbacks or chained Promises.

Reading Files Asynchronously

Here’s an example that reads lines from a file asynchronously:

const fs = require('fs');
const readline = require('readline');

async function* readLines(filePath) {
    const fileStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
    });

    for await (const line of rl) {
        yield line;
    }
}

async function printLines(filePath) {
    for await (const line of readLines(filePath)) {
        console.log(line);
    }
}

printLines('example.txt');

In this example:

readLines is an async generator function that reads lines from a file.

The for-await-of loop in printLines iterates over each line as it’s read.

Asynchronous iteration simplifies the handling of streams and other asynchronous sequences, making your code cleaner and more maintainable.

2. Rest/Spread Properties

The rest and spread properties were enhanced to support object literals. This feature allows you to work with objects more flexibly and concisely, making it easier to manipulate and clone objects.

Rest Properties

Rest properties allow you to collect the remaining properties of an object after some properties have been extracted. This feature is particularly useful for creating new objects with only a subset of the properties from an existing object.

const person = {
    name: 'John',
    age: 30,
    occupation: 'Engineer',
    country: 'USA'
};

// Destructuring with rest properties
const { name, ...rest } = person;

console.log(name);  // 'John'
console.log(rest);  // { age: 30, occupation: 'Engineer', country: 'USA' }

Spread Properties

Spread properties are used to copy the properties of one object into another. This is useful for creating copies of objects or combining multiple objects into one.

const person = {
    name: 'John',
    age: 30
};

// Using spread properties to create a new object
const updatedPerson = {
    ...person,
    occupation: 'Engineer'
};

console.log(updatedPerson);  // { name: 'John', age: 30, occupation: 'Engineer' }

Practical Use Cases

1- Merging Objects

Merging properties from multiple objects into one.

const defaultSettings = { theme: 'light', layout: 'grid' };
const userSettings = { layout: 'list' };

const finalSettings = { ...defaultSettings, ...userSettings };
console.log(finalSettings);  // { theme: 'light', layout: 'list' }
2- Cloning Objects

Creating a shallow copy of an object.

const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy);  // { a: 1, b: 2 }
3- Updating Objects

Creating a new object with updated properties.

const person = { name: 'Alice', age: 25 };
const updatedPerson = { ...person, age: 26 };
console.log(updatedPerson);  // { name: 'Alice', age: 26 }
4- Filtering Properties

Excluding certain properties from an object while copying.

const person = { name: 'Bob', age: 40, occupation: 'Developer' };
const { occupation, ...rest } = person;
console.log(rest);  // { name: 'Bob', age: 40 }

Notes

Both rest and spread properties handle shallow copies. Nested objects or arrays within the object are not deeply copied.

The order of spread properties in object literals matters. Later properties can overwrite earlier ones.

3. RegExp Enhancements

ECMAScript 2018 (ES9) introduced several important enhancements to regular expressions (RegExp) that make pattern matching and manipulation more powerful and flexible. These improvements include the addition of new RegExp flags and methods that simplify and extend the capabilities of regular expressions.

RegExp Enhancements in ES9

Named Capture Groups:

Named capture groups allow you to name the groups in your regular expressions, making the result of a match easier to access and more readable.

Syntax: (?<name>...)

const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = '2024-08-14';
const match = regex.exec(str);

if (match) {
    console.log(match.groups.year); // '2024'
    console.log(match.groups.month); // '08'
    console.log(match.groups.day);   // '14'
}
Named Groups in match Method:

The match method on strings now returns the named capture groups directly in the groups property of the match object.

const str = '2024-08-14';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = str.match(regex);

if (result) {
    console.log(result.groups.year); // '2024'
    console.log(result.groups.month); // '08'
    console.log(result.groups.day);   // '14'
}
Lookbehind Assertions:

Lookbehind assertions enable you to match a pattern only if it is preceded by a specific preceding pattern. This is useful for more complex matching scenarios.

Syntax: (?<=...) (positive lookbehind) and (?<!...) (negative lookbehind)

const regexPositive = /(?<=\d{2})\d{2}/; // Match two digits preceded by two digits
const strPositive = '1234';
const matchPositive = strPositive.match(regexPositive);
console.log(matchPositive[0]); // '34'

const regexNegative = /(?<!\d{2})\d{2}/; // Match two digits not preceded by two digits
const strNegative = '1234';
const matchNegative = strNegative.match(regexNegative);
console.log(matchNegative[0]); // '12' (matches because it's not preceded by two digits)
Unicode Property Escapes:

Unicode property escapes provide a way to match characters based on their Unicode properties, such as their category or script.

Syntax: \p{Property=Value} and \P{Property=Value}

// Match any character that is a Unicode letter
const regexLetter = /\p{L}/u;
console.log(regexLetter.test('A')); // true
console.log(regexLetter.test('1')); // false

// Match any character that is not a Unicode letter
const regexNotLetter = /\P{L}/u;
console.log(regexNotLetter.test('1')); // true
console.log(regexNotLetter.test('A')); // false

These enhancements make working with regular expressions more powerful and expressive, facilitating more complex and efficient text-processing tasks.

4. Promise.prototype.finally

In ECMAScript 2018 (ES9), the Promise.prototype.finally method was introduced to improve handling of asynchronous operations by providing a way to execute code after a Promise has settled, regardless of whether it was fulfilled or rejected.

The finally method allows you to specify a callback that will be executed when a Promise is settled, which means it has either been resolved or rejected. This can be useful for performing cleanup operations or for executing code that should run after the Promise operation, regardless of its outcome.

How It Works

  • When the Promise is fulfilled, the finally callback is executed after the then callback.
  • When the Promise is rejected, the finally callback is executed after the catch callback.
  • The finally callback does not affect the resolution or rejection value of the Promise.
const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // Simulate success or failure
            Math.random() > 0.5 ? resolve('Data loaded') : reject('Failed to load data');
        }, 1000);
    });
};

fetchData()
    .then(data => {
        console.log(data); // Handle success
    })
    .catch(error => {
        console.error(error); // Handle failure
    })
    .finally(() => {
        console.log('Operation complete'); // Always runs, regardless of success or failure
    });

Practical Use Cases

Cleanup Operations:

Use finally to perform cleanup operations regardless of whether an asynchronous task succeeds or fails. This can include things like hiding loading indicators or closing connections.

async function fetchData() {
    const loadingIndicator = document.getElementById('loading');
    loadingIndicator.style.display = 'block';

    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    } finally {
        loadingIndicator.style.display = 'none'; // Hide loading indicator
    }
}
Ensuring Code Execution:

Use finally to ensure that certain code is executed after a Promise has settled, such as logging or restoring state.

const processTask = async () => {
    try {
        await performTask();
    } catch (error) {
        console.error('Task failed:', error);
    } finally {
        console.log('Task processing complete');
    }
};

Important Considerations

Chaining: finally returns a Promise that resolves to the same value or rejection as the original Promise. This means it can be chained with then and catch.

fetchData()
    .finally(() => console.log('Cleaning up'))
    .then(() => console.log('Data processed'))
    .catch(err => console.error('Error:', err));

No Arguments: The finally callback does not receive any arguments and does not change the result of the original Promise.

The finally method enhances the flexibility of handling asynchronous operations by providing a way to ensure that certain actions are always performed, improving the reliability and cleanliness of your asynchronous code.

5. JSON.stringify Improvements

ECMAScript 2018 (ES9) introduced a useful enhancement to JSON.stringify(): the addition of a replacer function. This feature allows for finer control over how objects are serialized, providing more flexibility in the serialization process.

JSON.stringify Enhancements

Replacer Function:

When the replacer parameter is a function, it is called for each key-value pair in the object being stringified. The function can return a new value or undefined to omit the property from the JSON string.

Here’s an example of using a replacer function to filter out specific properties:

const person = {
    name: 'Alice',
    age: 30,
    password: 'secret'
};

// Replacer function that omits the 'password' property
function replacer(key, value) {
    if (key === 'password') {
        return undefined; // Omit the password property
    }
    return value; // Keep other properties unchanged
}

const jsonString = JSON.stringify(person, replacer, 2);
console.log(jsonString);
// Output:
// {
//   "name": "Alice",
//   "age": 30
// }

In this example, the replacer function is used to omit the password property from the resulting JSON string.

Replacer Array

If you provide an array as the replacer, only the properties listed in the array will be included in the resulting JSON string.

const person = {
    name: 'Alice',
    age: 30,
    password: 'secret'
};

// Replacer array that includes only 'name' and 'age'
const jsonString = JSON.stringify(person, ['name', 'age'], 2);
console.log(jsonString);
// Output:
// {
//   "name": "Alice",
//   "age": 30
// }

In this example, only the name and age properties are included in the resulting JSON string, while password is excluded.

6. Function.prototype.toString

In ECMAScript 2018 (ES9), the Function.prototype.toString method was enhanced to return the exact source code of a function as a string. This enhancement improved the accuracy of the output from toString for functions, especially in cases where functions are defined in a more complex or non-standard way.

The Function.prototype.toString method returns a string representing the source code of the function. Prior to ES9, this method might not have returned the exact source code in some cases, particularly for functions defined with certain syntax or in certain environments.

Behavior in ES9

In ES9, toString returns the exact source code of the function, including any whitespace or comments, as it appears in the source code. This means that if you define a function in a way that includes comments or is formatted with specific indentation, toString will return that exact formatting.

Simple Function
function add(a, b) {
    return a + b;
}

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

In this example, toString returns the exact source code of the add function.

Function with Comments
/**
 * Adds two numbers
 * @param {number} a - The first number
 * @param {number} b - The second number
 * @returns {number} The sum of the two numbers
 */
function add(a, b) {
    return a + b;
}

console.log(add.toString());
// Output:
// /**
//  * Adds two numbers
//  * @param {number} a - The first number
//  * @param {number} b - The second number
//  * @returns {number} The sum of the two numbers
//  */
// function add(a, b) {
//     return a + b;
// }

Here, toString includes the comments as part of the source code.

Arrow Function
const multiply = (x, y) => x * y;

console.log(multiply.toString());
// Output:
// (x, y) => x * y

The toString method returns the exact source code of the arrow function.

Impact and Usage
Debugging and Logging:

Enhanced toString is useful for debugging purposes where you need to log or inspect the exact source code of functions.

Code Analysis and Transformation:

Tools that analyze or transform code (e.g., minifiers or code formatters) can use the exact source code to better understand and manipulate functions.

Serialization:

In some cases, you might want to serialize functions for storage or transfer, and having the exact source code is beneficial.

Function Wrapping:

When wrapping functions (e.g., for logging or profiling), you might want to retain the original function’s source code for accurate representation.

Object.prototype.__proto__

In ECMAScript 2018 (ES9), the __proto__ property was standardized and formalized as a part of the language specification. Although __proto__ had been widely supported in various JavaScript engines prior to ES9, its standardization aimed to provide a consistent and reliable way to access and modify an object’s prototype.

The __proto__ property is an accessor property that provides a way to get or set the prototype of an object. It acts as a shortcut to interact with the internal [[Prototype]] property, which is the actual mechanism JavaScript uses for prototypal inheritance.

Key Points

Getter and Setter:
  • When used as a getter, __proto__ returns the prototype of the object.
  • When used as a setter, __proto__ sets the prototype of the object to the specified value.
Standardization:

Before ES9, __proto__ was widely implemented but was not part of the ECMAScript specification. ES9 formalized its behavior to ensure consistent implementation across different JavaScript engines.

Avoiding __proto__:

While __proto__ is now standardized, it is generally recommended to use Object.getPrototypeOf() and Object.setPrototypeOf() for interacting with object prototypes. These methods provide a more explicit and standardized approach.

Examples

Getting the Prototype
const obj = {};
const proto = Object.create(null); // Create an object with no prototype
obj.__proto__ = proto;

console.log(obj.__proto__ === proto); // true
Setting the Prototype
const proto1 = { foo: 'bar' };
const proto2 = { baz: 'qux' };

const obj = {};
obj.__proto__ = proto1;

console.log(obj.foo); // 'bar'

obj.__proto__ = proto2;

console.log(obj.foo); // undefined (proto1 is no longer the prototype)
console.log(obj.baz); // 'qux'

Best Practices

Prefer Object.getPrototypeOf and Object.setPrototypeOf:
  • For better readability and adherence to standards, use these methods instead of __proto__.
const obj = {};
const proto = { greet: 'hello' };

// Setting prototype using Object.setPrototypeOf
Object.setPrototypeOf(obj, proto);

console.log(obj.greet); // 'hello'

// Getting prototype using Object.getPrototypeOf
console.log(Object.getPrototypeOf(obj) === proto); // true
Avoid Changing Prototypes Dynamically:
  • Changing an object’s prototype at runtime (e.g., using __proto__ or Object.setPrototypeOf) can lead to performance issues and should generally be avoided in favor of designing object hierarchies at object creation time.

If you missed my recent posts on ES6 (ECMAScript 2015), ES7 (ECMAScript 2016) and ES8 (ECMAScript 2017), you can catch up by clicking the links below to explore their features and updates.

Click here to explore ES6 (ECMAScript 2015)
Click here to explore ES7 (ECMAScript 2016)
Click here to explore ES8 (ECMAScript 2017)

Happy coding!

FAQs

  • Array.prototype.flat(depth) flattens nested arrays into a single array. The depth argument specifies how deep a nested array structure should be flattened. The default depth is 1.
  • Array.prototype.flatMap(callback) first maps each element using a mapping function, then flattens the result into a new array.

let arr = [1, [2, [3, [4]]]];
console.log(arr.flat(2)); // [1, 2, 3, [4]]

let arr2 = [1, 2, 3, 4];
console.log(arr2.flatMap(x => [x * 2])); // [2, 4, 6, 8]

Object.fromEntries() is used to convert a list of key-value pairs (such as an array of arrays or a Map) into an object.

let entries = [['name', 'John'], ['age', 30]];
let obj = Object.fromEntries(entries);
console.log(obj); // { name: 'John', age: 30 }

  • String.prototype.trimStart() removes whitespace from the beginning of a string.
  • String.prototype.trimEnd() removes whitespace from the end of a string.

let str = ' Hello World! ';
console.log(str.trimStart()); // 'Hello World! '
console.log(str.trimEnd()); // ' Hello World!'

The Optional Catch Binding feature allows you to omit the error parameter in a catch block if it is not needed. This helps simplify code when the error is not used.

try {
// Code that may throw an error
} catch {
// Error handling without using the error parameter
}

In ES9, Function.prototype.toString() was standardized to return a more consistent and reliable source code representation of the function. This is particularly useful for debugging and development tools.

function example() { return 42; }
console.log(example.toString()); // 'function example() { return 42; }'

ES9 improved the handling of certain edge cases in JSON.stringify(), ensuring that it correctly handles special scenarios like objects with a Symbol.toJSON method.

You can use feature detection libraries such as Babel to transpile code for compatibility with older environments. Additionally, check compatibility tables on resources like MDN Web Docs or Can I use for specific feature support.

4 thoughts on “Everything You Need to Know About ECMAScript 2018 (ES9)

Leave a Reply

Your email address will not be published. Required fields are marked *