26 mins read

What’s New in ECMAScript 2020 (ES11)

Hey, JavaScript Lovers!

ECMAScript 2020, also known as ES2020 or ES11, introduced several new features and improvements to JavaScript.

Some of the key features introduced in ES11 include:

  1. Optional Chaining (?.)
  2. Nullish Coalescing Operator (??)
  3. BigInt
  4. Promise.allSettled
  5. Dynamic Import
  6. import.meta
  7. Module namespace exports
  8. String.prototype.matchAll
  9. WeakRef
  10. FinalizationRegistry

1. Optional Chaining (?.)

Optional chaining (?.) is a feature introduced in ECMAScript 2020 (ES11) that allows you to safely access deeply nested properties in an object without having to check each level for existence. This feature can help you avoid errors when trying to access properties on objects that might be null or undefined.

Here’s a quick overview of how it works:

Object Property Access: object?.property
const user = { name: 'Alice' };
console.log(user?.name); // Output: 'Alice'
console.log(user?.age); // Output: undefined
Method Calls: object?.method()
const user = {
  greet() { return 'Hello'; }
};
console.log(user?.greet()); // Output: 'Hello'

const noUser = null;
console.log(noUser?.greet()); // Output: undefined
Array Access: array?.[index]
const users = ['Alice', 'Bob'];
console.log(users?.[1]); // Output: 'Bob'

const empty = null;
console.log(empty?.[0]); // Output: undefined
Chaining Multiple Levels
const user = {
  profile: {
    contact: {
      email: 'alice@example.com'
    }
  }
};
console.log(user?.profile?.contact?.email); // Output: 'alice@example.com'
console.log(user?.profile?.address?.city); // Output: undefined

Benefits

Prevents Errors: Optional chaining helps prevent runtime errors when trying to access properties on objects that could be null or undefined.

Cleaner Code: It reduces the need for multiple null checks, leading to cleaner and more readable code.

Caveats

Performance: While optional chaining is convenient, it can have a slight impact on performance in deeply nested structures compared to traditional null checks.

Compatibility: Ensure your environment supports ES2020 features or use a transpiler like Babel if you need compatibility with older browsers or environments.

Overall, optional chaining is a powerful feature that simplifies working with nested data structures and makes your code more robust and easier to maintain.

2. Nullish Coalescing Operator (??)

The nullish coalescing operator (??) was introduced in ECMAScript 2020 (ES11) and provides a way to handle null or undefined values more precisely than the logical OR operator (||).

Here’s a brief overview of how it works:

Behavior

  1. If value1 is neither null nor undefined, then result will be value1.
  2. If value1 is null or undefined, then result will be value2.
let user = {
  name: "Alice",
  age: null
};

// Using nullish coalescing
let age = user.age ?? 25;
console.log(age); // Output: 25 (because user.age is null)

// Using logical OR
let ageOr = user.age || 25;
console.log(ageOr); // Output: 25 (but note this would also return 25 for other falsy values like 0, '')

Key Differences from Logical OR (||)

Logical OR (||) treats all falsy values (0, NaN, "", false, null, undefined) as the same and will coalesce to the right-hand side if the left-hand side is any of these falsy values.

Nullish coalescing (??) specifically targets null and undefined only. Other falsy values like 0, NaN, or "" are not considered nullish and will not trigger the right-hand side.

Use Cases

Default Values for Missing Properties: Use ?? to provide default values only when a variable is explicitly null or undefined, avoiding unintended behavior with other falsy values.

Handling Optional Values: For cases where you need a default value for optional or potentially missing values, ?? ensures you handle only null or undefined cases.

Important Note

The nullish coalescing operator has lower precedence than most operators (except for the ternary conditional operator). Therefore, if you combine ?? with other operators, be mindful of how they interact:

let value = 0;
let result = value ?? (value === 0 ? 'zero' : 'not zero');
console.log(result); // Output: 0

In this example, value is 0, which is not null or undefined, so the nullish coalescing operator returns 0, and the right-hand side of the ?? is not evaluated.

3. BigInt

BigInt, introduced in ES11, allows you to work with integers larger than Number.MAX_SAFE_INTEGER (2^53 – 1) and smaller than Number.MIN_SAFE_INTEGER (-2^53 + 1).

Key Features

  • Syntax: Add an n at the end of an integer literal to create a BigInt.
  • Arithmetic Operations: Supports addition, subtraction, multiplication, division (floor division), and modulus operations.
  • Comparison: Can be compared with other BigInts and numbers but always returns false when compared with regular numbers directly.
  • Methods: Has methods like toString(), toLocaleString(), and valueOf() for conversion and manipulation.
let largeNumber = 1234567890123456789012345678901234567890n;
let anotherLargeNumber = 9876543210987654321098765432109876543210n;

let sum = largeNumber + anotherLargeNumber;
console.log(sum); // Output: 11111111101111111111111111111111111111n

Usage Notes

Operations with Regular Numbers: You cannot mix BigInts with regular numbers directly. For example, bigIntValue + 1 will throw an error.

Interoperability: To work with BigInt values and numbers together, you need to convert them explicitly.

BigInt is ideal for scenarios requiring precise integer arithmetic beyond the range of the Number type, like financial calculations or cryptography.

4. Promise.allSettled

Promise.allSettled, introduced in ES11, is a method that allows you to handle multiple promises and get their results once all of them have either fulfilled or rejected.

Behavior

Returns a Promise: This method returns a single Promise that resolves after all the given promises have either resolved or rejected.

Result Array: The resolved promise yields an array of objects describing the outcome of each promise. Each object has two properties:

  • status: "fulfilled" or "rejected".
  • value or reason: Contains the result if fulfilled, or the reason if rejected.
let p1 = Promise.resolve(1);
let p2 = Promise.reject(new Error("Failure"));
let p3 = Promise.resolve(3);

Promise.allSettled([p1, p2, p3])
  .then(results => {
    console.log(results);
    /*
      Output:
      [
        { status: "fulfilled", value: 1 },
        { status: "rejected", reason: Error: Failure },
        { status: "fulfilled", value: 3 }
      ]
    */
  });

Use Cases

Handling Multiple Promises: Useful when you want to wait for all promises to settle, regardless of their outcomes.

Aggregating Results: Allows you to handle both success and failure outcomes in a unified way, often used when you need to process results from multiple asynchronous operations.

Promise.allSettled is ideal for scenarios where you need to ensure that all promises have finished executing, without halting due to a single promise’s rejection.

5. Dynamic Import

Dynamic import, introduced in ES11, allows you to load JavaScript modules asynchronously using the import() function. This feature is useful for code-splitting and loading modules on demand.

Behavior

Returns a Promise: The import() function returns a promise that resolves to the module object.

Module Specifier: The argument can be a string literal or an expression that resolves to a string, which should be the path or URL to the module.

// Dynamically import a module
import('./myModule.js')
  .then(module => {
    // Use the module's exported functions or variables
    module.myFunction();
  })
  .catch(error => {
    console.error('Failed to load module:', error);
  });

Use Cases

  • Code Splitting: Load parts of your application only when needed, improving initial load time.
  • Conditional Imports: Load modules based on user actions or conditions, enhancing performance and user experience.
  • Feature Flags: Dynamically load features or components based on feature flags or configuration settings.

Dynamic imports are ideal for optimizing performance and managing dependencies in a modular and flexible way.

6. import.meta

import.meta is an object introduced in ES11 (ECMAScript 2020) that provides metadata about the current module. It allows access to module-specific information and is often used with the dynamic import() function.

Module URL: import.meta.url provides the URL of the module, which is useful for constructing relative paths to other resources.

Suppose you have a module located at https://example.com/scripts/myModule.js. Using import.meta.url, you can obtain the URL of the current module:

// myModule.js
console.log(import.meta.url);
// Output: 'https://example.com/scripts/myModule.js'

// You can use this URL to resolve paths relative to the module

Use Cases

  • Dynamic Resource Loading: Construct paths relative to the module URL for dynamically loading resources, such as images or other modules.
  • Debugging and Logging: Get information about the module’s URL for debugging or logging purposes.
  • Modular Architecture: When working with modular applications, use import.meta.url to handle paths and dependencies relative to the module.

Key Points

Static Properties: import.meta itself does not have many static properties or methods; the primary feature is import.meta.url.

Environment-Specific: The exact behavior and availability of import.meta can vary depending on the module system or environment (e.g., browsers, Node.js).

Limitations

  • Browser Support: Ensure your target environment supports import.meta and import.meta.url, as support may vary across different JavaScript engines or versions.
  • Module Only: import.meta is only available in ES modules and not in CommonJS or other module systems.

Overall, import.meta is a powerful feature for modules, providing useful metadata and aiding in dynamic path resolution.

7. Module namespace exports

In ES11 (ECMAScript 2020), module namespace exports provide a way to export all named exports from one module and import them into another module as a single object. This allows you to create a consolidated object that holds all the exports from a module, making it easier to manage and work with grouped exports.

Suppose you have a module math.js with multiple named exports:

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

You can import all the named exports from math.js as a single object in another module:

// main.js
import * as math from './math.js';

console.log(math.add(2, 3));         // Output: 5
console.log(math.subtract(5, 2));    // Output: 3
console.log(math.multiply(4, 3));    // Output: 12
console.log(math.divide(10, 2));     // Output: 5

In this example, the math object will contain all the named exports from math.js, accessible as properties of that object.

Benefits

  • Organized Imports: Group all exports from a module into a single object, which can make code more readable and easier to manage.
  • Namespace Management: Avoid name conflicts and manage multiple related exports in a clean and structured way.
  • Simplified Code: Importing multiple items from a module in one statement reduces the number of import statements needed.

Key Points

Namespace Object: The imported object will contain all named exports from the module, but not the default export. If a module has a default export, you need to import it separately.

Dynamic Imports: You can use namespace imports with dynamic imports as well. For example:

import('./math.js')
  .then(module => {
    console.log(module.add(2, 3));  // Output: 5
  });

Read-Only: The imported namespace object is read-only. You cannot modify the exports of the module through the namespace object.

Example with Default Export

If a module has both named and default exports, you can import them like this:

// utils.js
export const pi = 3.14;
export function square(x) {
  return x * x;
}
export default function cube(x) {
  return x * x * x;
}

// main.js
import cube, * as utils from './utils.js';

console.log(cube(3));        // Output: 27
console.log(utils.pi);      // Output: 3.14
console.log(utils.square(4)); // Output: 16

In this example, cube is the default export, and utils is a namespace object containing all the named exports.

8. String.prototype.matchAll

String.prototype.matchAll is a method introduced in ES11 (ECMAScript 2020) that allows you to find all matches of a regular expression in a string and return them as an iterator of RegExp match objects. This method is particularly useful for extracting multiple matches from a string, especially when the regular expression has the global (g) flag.

Parameters

regexp: A regular expression object. It must have the global (g) flag set; otherwise, matchAll will throw a TypeError.

Returns

An iterator of RegExp match objects. Each match object contains the full match and capturing groups.

Here’s a basic example showing how to use matchAll to find all matches of a regular expression in a string:

const text = 'The quick brown fox jumps over the lazy dog. The fox is clever.';

// Regular expression with the global flag
const regex = /fox/g;

const matches = text.matchAll(regex);

for (const match of matches) {
  console.log(match);
}
/*
Output:
[
  'fox',
  index: 16,
  input: 'The quick brown fox jumps over the lazy dog. The fox is clever.',
  groups: undefined
]
[
  'fox',
  index: 39,
  input: 'The quick brown fox jumps over the lazy dog. The fox is clever.',
  groups: undefined
]
*/

Example with Capturing Groups

If your regular expression includes capturing groups, matchAll will provide additional details in each match object:

const text = 'My phone number is 123-456-7890 and my office number is 987-654-3210.';

// Regular expression with capturing groups
const regex = "/(\d{3})-(\d{3})-(\d{4})/g";

const matches = text.matchAll(regex);

for (const match of matches) {
  console.log(match);
}
/*
Output:
[
  '123-456-7890',
  '123',
  '456',
  '7890',
  index: 21,
  input: 'My phone number is 123-456-7890 and my office number is 987-654-3210.',
  groups: undefined
]
[
  '987-654-3210',
  '987',
  '654',
  '3210',
  index: 50,
  input: 'My phone number is 123-456-7890 and my office number is 987-654-3210.',
  groups: undefined
]
*/

Key Points

Global Flag Required: The regular expression must have the global (g) flag for matchAll to work. Without the g flag, the method will throw an error.

Iterator: matchAll returns an iterator, not an array. You can iterate over it using a for...of loop or convert it to an array if needed:

const matchesArray = Array.from(text.matchAll(regex));
console.log(matchesArray);

Match Object: Each match object contains:

  • The full match as the first element.
  • Capturing groups as subsequent elements.
  • The index of the match within the string.
  • The input string that was searched.

Use Cases

  • Extracting Multiple Matches: When you need to extract all occurrences of a pattern with capturing groups.
  • Iterating Matches: For scenarios where you need to perform operations on each match.

String.prototype.matchAll is a powerful addition to JavaScript, making it easier to work with multiple matches of a regular expression and providing detailed match information for more complex string processing tasks.

9. WeakRef

WeakRef is a feature introduced in ES11 (ECMAScript 2020) that provides a way to hold a weak reference to an object. A weak reference is a reference that does not prevent the garbage collector from reclaiming the object if there are no other strong references to it. This can be useful for cases where you want to cache objects or manage memory without preventing objects from being collected.

Features

  • Weak Reference: Unlike strong references, weak references do not keep an object alive. If the object referenced by a WeakRef is only weakly referenced (i.e., there are no strong references to it), it can be garbage collected.
  • Handling Weak References: To access the referenced object, you use the deref() method on a WeakRef instance.

Here’s a basic example demonstrating the use of WeakRef:

let obj = { name: 'John' };

// Create a WeakRef to the object
let weakRef = new WeakRef(obj);

// Access the object through WeakRef
let derefObj = weakRef.deref();
console.log(derefObj); // Output: { name: 'John' }

// Delete the strong reference
obj = null;

// Attempt to access the object again
derefObj = weakRef.deref();
console.log(derefObj); // Output: undefined (object may have been garbage collected)

Key Points

  • Garbage Collection: A WeakRef does not prevent the garbage collection of the object it references. If the only references to the object are weak references, the object may be collected by the garbage collector.
  • deref() Method: The deref() method returns the referenced object if it is still available or undefined if the object has been collected.
  • Use Cases:
    Caching: To cache objects without preventing their garbage collection. If you cache objects using weak references, the garbage collector can reclaim them when memory is needed.
    Memory Management: For scenarios where you want to keep a reference to an object but do not want to prevent it from being collected, such as implementing certain kinds of caches or references that should not prevent objects from being collected.

Limitations

No Strong Guarantees: There are no guarantees that the object will still be available when you call deref(). The object may have been collected by the time you try to access it.

No Notifications: WeakRef does not provide notifications or callbacks when the object is collected.

Example with Cache

Here’s an example of how WeakRef can be used in a simple caching scenario:

class Cache {
  constructor() {
    this.cache = new Map();
  }

  add(key, value) {
    // Use WeakRef for values to allow garbage collection
    this.cache.set(key, new WeakRef(value));
  }

  get(key) {
    const weakRef = this.cache.get(key);
    if (weakRef) {
      const value = weakRef.deref();
      if (value) {
        return value;
      } else {
        // Remove the entry if the object was garbage collected
        this.cache.delete(key);
      }
    }
    return undefined;
  }
}

const cache = new Cache();

let obj = { name: 'Alice' };
cache.add('user', obj);

// Accessing the cached object
console.log(cache.get('user')); // Output: { name: 'Alice' }

// Remove strong reference
obj = null;

// At some point, the garbage collector might collect the object
console.log(cache.get('user')); // Output: undefined (if the object was collected)

In this example, the Cache class uses WeakRef to store objects in a way that allows them to be garbage collected if there are no other strong references. This approach helps manage memory more efficiently by allowing objects to be collected when they are no longer needed.

10. FinalizationRegistry

FinalizationRegistry is a feature introduced in ES11 (ECMAScript 2020) that provides a way to register cleanup callbacks that are invoked when an object is garbage collected. This can be useful for managing resources or performing cleanup tasks related to objects that are no longer in use.

Creating a FinalizationRegistry

const registry = new FinalizationRegistry((heldValue) => {
  // Cleanup code here
});

The callback function you provide will be called with the heldValue when the registered object is garbage collected.

Registering an Object

registry.register(object, heldValue, unregisterToken);
  • object: The object you want to monitor for garbage collection.
  • heldValue: An optional value that will be passed to the callback when the object is collected.
  • unregisterToken: An optional token used to unregister the object. If provided, it can be used with registry.unregister(unregisterToken) to remove the registration.

Unregistering an Object

registry.unregister(unregisterToken);

This method removes the registration of the object from the registry, preventing the callback from being called when the object is garbage collected.

Here’s a basic example demonstrating how to use FinalizationRegistry:

// Create a FinalizationRegistry with a cleanup callback
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Cleaning up: ${heldValue}`);
});

// Function to create and register an object
function createAndRegister() {
  let obj = { name: 'Resource' };
  // Register the object with a held value
  registry.register(obj, 'Resource Cleanup');
  // When obj is garbage collected, the callback will be invoked
}

// Call the function and create a registered object
createAndRegister();

// At this point, obj is still reachable
console.log('Object is still in use.');

// Remove all strong references to obj
// The garbage collector will eventually collect the object and invoke the callback

Key Points

  • Callback Invocation: The callback provided to FinalizationRegistry is invoked when the registered object is garbage collected. This allows you to perform cleanup tasks such as releasing resources or performing other finalization work.
  • Weak References: The FinalizationRegistry does not prevent the garbage collection of the objects it monitors. It only helps you manage cleanup when those objects are collected.
  • Unregistering: You can use the optional unregisterToken to remove a registration from the registry before the object is collected, preventing the callback from being invoked.
  • Garbage Collection Timing: The actual time when the cleanup callback is invoked is not deterministic. It depends on when the garbage collector decides to collect the objects.

Use Cases

Resource Management: Use FinalizationRegistry to release resources such as file handles, network connections, or other external resources when objects are no longer needed.

Cleanup Tasks: Perform cleanup tasks or notifications when objects are garbage collected.

Example with Resource Management

Here’s an example showing how FinalizationRegistry can be used to manage resources:

class Resource {
  constructor(name) {
    this.name = name;
    console.log(`Resource ${name} created.`);
  }

  close() {
    console.log(`Resource ${this.name} closed.`);
  }
}

// Create a FinalizationRegistry for cleanup
const registry = new FinalizationRegistry((resource) => {
  resource.close();
});

// Function to create a resource and register it
function createResource(name) {
  const resource = new Resource(name);
  registry.register(resource, resource);
  return resource;
}

// Create and register a resource
createResource('MyResource');

// Remove strong references
// The resource will be cleaned up when garbage collected

In this example, the FinalizationRegistry ensures that the close method of the Resource class is called when the resource is garbage collected, helping manage and release resources effectively.

If you missed my recent posts on ES6 (ECMAScript 2015), ES7 (ECMAScript 2016), ES8 (ECMAScript 2017) ES9 (ECMAScript 2018) and ES10 (ECMAScript 2019), 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)
Click here to explore ES9 (ECMAScript 2018)
Click here to explore ES10 (ECMAScript 2019)

Happy coding!

FAQs

Optional Chaining (?.) is a syntax feature that allows you to safely access nested properties of an object without having to check if each level exists. If a reference is null or undefined, the expression short-circuits and returns undefined.

const user = { profile: { name: 'Alice' } };
console.log(user.profile?.name); // 'Alice'
console.log(user.profile?.age); // undefined
console.log(user.address?.city); // undefined

The Nullish Coalescing Operator (??) returns the right-hand side operand when the left-hand side operand is null or undefined. This is different from the logical OR operator (||), which also considers other falsy values (like 0, NaN, and '').

let value = null;
let defaultValue = 10;
console.log(value ?? defaultValue); // 10

value = 0;
console.log(value ?? defaultValue); // 0

Dynamic Import allows you to import JavaScript modules asynchronously using the import() function. This can be used to load modules on demand and improve the performance of applications by reducing initial load times.

import('./module.js')
.then(module => {
module.doSomething();
})
.catch(err => {
console.error('Failed to load module:', err);
});

BigInt is a new primitive type that allows you to represent and work with integers larger than 2^53 - 1, which is the maximum safe integer in JavaScript. It is useful for applications that require high-precision arithmetic.

const bigNumber = BigInt('9007199254740991');
const anotherBigNumber = BigInt(123456789012345678901234567890);
console.log(bigNumber + 1n); // 9007199254740992n

These methods provide improved handling for objects:

  • Object.entries(obj) returns an array of [key, value] pairs.
  • Object.values(obj) returns an array of values.
  • Object.keys(obj) returns an array of keys.

const obj = { a: 1, b: 2, c: 3 };
console.log(Object.entries(obj)); // [['a', 1], ['b', 2], ['c', 3]]
console.log(Object.values(obj)); // [1, 2, 3]
console.log(Object.keys(obj)); // ['a', 'b', 'c']

String.prototype.matchAll() returns an iterator of all matches for a regular expression, including capturing groups, in a string. This is useful for iterating over all matches and their groups.

const regex = /(\d+)/g;
const str = 'There are 15 apples and 23 oranges.';
const matches = str.matchAll(regex);

for (const match of matches) {
console.log(match[0]); // '15', '23'
console.log(match[1]); // '15', '23'
}

Promise.allSettled() returns a promise that resolves after all of the given promises have either resolved or rejected. It provides an array of objects representing the outcome of each promise, including whether it was fulfilled or rejected.

const p1 = Promise.resolve(1);
const p2 = Promise.reject('error');
const p3 = Promise.resolve(3);

Promise.allSettled([p1, p2, p3])
.then(results => {
console.log(results);
// [{ status: 'fulfilled', value: 1 }, { status: 'rejected', reason: 'error' }, { status: 'fulfilled', value: 3 }]
});

To check for support of ECMAScript 2020 features, you can use:

  • Feature Detection Tools: Libraries like Babel can help you write code compatible with older environments.
  • Compatibility Tables: Resources such as MDN Web Docs or Can I use provide up-to-date information on feature support across different browsers and environments.

2 thoughts on “What’s New in ECMAScript 2020 (ES11)

Leave a Reply

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