ES2020: Hello BigInt and dynamic importing

OpenJavaScript 0

Last updated: November 30, 2021.

The 2020 update to the JavaScript language includes many practically useful native solutions for tricky problems and also features the addition of a new primitive data type, BigInt.

In this article we are going to cover the essentials of each of the following new features introduced by ECMAScript 2020 in plain English:

  1. The BigInt primitive type
  2. Dynamic importing using import()
  3. globalThis
  4. matchAll()
  5. Promise.allSettled()
  6. The optional chaining operator (?.)
  7. The nullish coalescing operator (??)

#1: BigInt

BigInt is a new primitive data type that enables the use of very large integers without any loss of precision. This is not the case with the primitive type Number.

Here is the issue with using Number and a very large integer value:

alert(99999999999999999); // 100000000000000000

This is because Number can only guarantee numeric precision to a limit. We can see this by calling Number.MAX_SAFE_INTEGER:

alert(Number.MAX_SAFE_INTEGER); // 9007199254740991

This returned value of 9007199254740991 is the greatest value of Number that can be worked with without risking a loss of precision. This is problematic if one wants to make a precise calculation or is working with a number that must not lose its precision, such as an ID value.

BigInt has been introduced to resolve this issue.

We can create a value of type BigInt by calling the in-built BigInt object directly, passing in the integer value we want to generate:

const bigInteger = BigInt("99999999999999999");
console.log(bigInteger) // 99999999999999999n

We can also create a BigInt more conveniently by simply adding n to the end of a number:

const bigInteger = 99999999999999999n
console.log(bigInteger) // 99999999999999999n

BigInt cannot be used in a calculation with a value of type Number.

However, a value of type Number can easily be converted to a BigInt by passing it into the BigInt object. And a BigInt value converted to a Number type by passing it into Number().

const bigIntValue = 2n;
const numberValue = 2;
 
// BigInt to Number with Number()
console.log(Number(bigIntValue) + numberValue); // 4
 
// Number to type BigInt with BigInt()
console.log(bigIntValue + BigInt(numberValue)); // 4

// BigInt + Number
console.log(bigIntValue + numberValue); // Type error

Making a comparison between two identical values of type Number and BigInt will return true when testing for standard equality (==). But will return false in a test of strict equality (===) because the values are stored in different types.

const x = BigInt("2");
const y = 2;
 
alert(x==y); // true
alert(x===y); // false

#2: Dynamic importing

ECMAScript 2020 now allows for JavaScript modules to be imported on-demand.

Until now, the import syntax has only supported ‘static’ importing. For example:

import * as module from "./module.js";

In the above line of code, the importing of module.js is not conditional. Thus, it can cause unnecessary loading when we start an application.

To change this, ES11 has introduced the import() method. And with it, we can make the loading of a module dynamic.

For example, we may choose to only load a module after a user has clicked a button.

To do so, we define only a file path at the beginning our our script (no import).

Then, when convenient, we use the import() method to import a module. Once imported, its functionality is available for use in our application.

const btn = document.getElementById("btn");
const modulesPath = "./modules";

btn.addEventListener("click", async () => {
    const module = await import(`${modulesPath}/someFunctionality.js`);
})

module.someMethod()

#3: globalThis

When running JavaScript in the browser, the global object can be accessed with window.

If using Node.js, the global object is available by calling global.

And in a web worker environment, the global object can only be accessed using self.

See an issue here?

One of the great strengths of JavaScript is that it can be used in different environments. Differences in calling the global object, however, can lead to errors if the same script is run in different environment.

To make JavaScript more cross-environment friendly, ES11 introduces the globalThis object. It can be used across JavaScript environments to access the global object:

// JavaScript in the browser
console.log(globalThis); // Window {...}

// Node.js
console.log(globalThis); // Object [global] {...}

// Using web workers
console.log(globalThis); // DedicatedWorkerGlobalScope {...}

#4: matchAll()

The matchAll method takes a regular expression and returns an iterator with an array of information for each match it finds.

For example:

const string = "Learn JavaScript at OpenJavaScript";
const regexp = /JavaScript/g;
const matches = string.matchAll(regexp);

for (match of matches) {
  console.log(match);
}

This returns:

['JavaScript', index: 6, input: 'Learn JavaScript at OpenJavaScript', groups: undefined]
['JavaScript', index: 24, input: 'Learn JavaScript at OpenJavaScript', groups: undefined]

When using matchAll(), note that it is necessary include the global flag (/g) at the end of the regular expression. Otherwise JavaScript will throw a type error.

#5: Promise.allSettled()

The Promise.allSettled() method takes an array of input promises. It then itself returns a promise when all of the input promises are settled (either resolved or rejected).

It is different from Promise.all(), which rejects immediately if any of the input promises rejected.

Upon the settling of all input promises, Promise.allSettled() returns an array of objects with information about the settling of each promise.

In the following example, the output from Promise.allSettled() is returned after 1 second as all promises are settled after this time.

const promise1 = new Promise((resolve, reject) => setTimeout(reject, 1000));
const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 200));
const promise3 = new Promise((resolve, reject) => setTimeout(reject, 500));
const promises = [promise1, promise2, promise3];

Promise.allSettled(promises)
  .then((outcomes) => outcomes.forEach((outcome) => console.log(outcome.status)));

// Output after 1 second:
// rejected
// fulfilled
// rejected

#6: Optional chaining operator (.?)

The optional chaining operator can be used for checking for the existence of deeply nested properties in objects.

The advantage of using the optional chaining operator is that JavaScript will return undefined rather than throw an error is no value exists.

For example, in the below code, we successfully extract the values for firstName and lastName.

Then, we try to access the value for telephone. This does not exist but it is not deeply nested (contact exists). So JavaScript returns undefined.

But when we test for work, which is nested one level deeper, JavaScript throws an error rather than returning undefined.

const user = {
    name: {
        firstName: "Bob",
        lastName: "Reeves"
    },
    contact: {
        email: "bobreeves80509@email.com"
    }
}

console.log(user.name.firstName); // Bob
console.log(user.name.lastName); // Reevees

console.log(user.contact.telephone); // undefined
console.log(user.contact.telephone.work); // Error

We can ensure that undefined is returned in all cases for non-existent properties by using the optional chaining operator.

To prevent the error in the above example, it is enough to place the optional chaining operator after telephone. In this case, telephone is effectively treated as existing, and work will return undefined (because it is not deeply nested).

However, in practice, we may choose to use optional chaining throughout a property call so that no error is generated. This would even be the case with a completely empty object, in which case undefined would also be returned.

const user = {
    name: {
        firstName: "Bob",
        lastName: "Reeves"
    },
    contact: {
        email: "bobreeves80509@email.com"
    }
}

console.log(user.contact.telephone?.work); // undefined
console.log(user?.contact?.telephone?.work); // undefined

Nullish coalescing operator (??)

The nullish coalescing operator conditionally returns its right-hand value when its left-hand value is either null or undefined. Otherwise, it returns its left-hand value.

This is different from the logical OR operator (||), which returns its right-hand value when its left-hand value is ‘falsy’. This is a broader term that also includes 0 and an empty string value ("").

Some examples:

// With null coalescing operator (??)
const test1 = undefined ?? "String1"
const test2 = null ?? "String2"
const test3 = 0 ?? "String 3"
const test4 = "" ?? "String 4"

console.log(test1,test2,test3,test4)
// String1
// String2
// 0
//

// With logical OR operator (||)
const test5 = undefined || "String1"
const test6 = null || "String2"
const test7 = 0 || "String 3"
const test8 = "" || "String 4"

console.log(test5,test6,test7,test8)
// String1
// String2
// String3
// String4

Essentially, then, the nullish coalescing operator (??) can be interpreted as a stricter version of the logical OR operator (||).