It’s not easy keeping up with all the new features the ECMAScript spec brings us every year. Let’s catch up with the highlights from ES11.
The JavaScript juggernaut never stops. There are a number of features introduced by the “living spec” every year. Then it takes some time for the browsers, Node.js, etc. to roll them in, and before you know it, there’s a big pile of new JavaScript features you haven’t tried.
It’s not too late. Here I’ve rounded up the ES11 (ECMAScript 11, aka ECMAScript 2020) features that you may have missed. They include a number of ergonomic and other modern improvements. Let’s take a look.
Optional chaining
Optional chaining is one of those simple-but-effective niceties that just makes life a little easier. This feature allows you to navigate object and function chains with a shorthand for dealing with null or undefined values.
Optional chaining works for simple cases like nested object data, for functions that return non-existent values, and even for functions that don’t exist as member methods on objects. So you can do things like we see in Listing 1.
Listing 1. Optional chaining examples
box = {
innerBox: {},
nullFunction: function() { return null; },
// non-existent method foo() and members bar
}
// with optional chaining:
if (box?.innerBox?.foo){ }// navigate object graph safely
// old style without optional chaining:
if (box.innerBox && box.innerBox.foo){ }
//also works for functions:
box.nullFunction()?.foo
// and nonexistent methods and members:
box?.foo() && box?.bar
The code is more concise in expressing just what you want: If the thing exists, navigate to it; if not, return undefined.
globalThis
Another sensible addition to the core language, globalThis
, is an abstraction of the global object available to the current context. The most well-known and ancient of these is the window object found in JavaScript running in the browser. But environments like Node.js and web workers have their own root objects that serve precisely the same purpose: global and self, respectively.
globalThis
makes code that is more portable (and eliminates existence checks) by wrapping all of these root objects in the one identifier that resolves to the global root object in whatever environment the code is executing in.
BigInt
Before ES11 the largest integer that could be referenced in JavaScript safely was Number.MAX_SAFE_INTEGER, which resolves to 9007199254740991 (aka 2^53 – 1). This may not be a daily problem for some of us, but for many applications it’s an amusingly tiny magnitude requiring programmers to use a wrapper like big-integer. (Note that the big-integer library is still useful as a polyfill.)
When using the traditional Number type to represent such large numbers, you will encounter unexpected rounding. (Notice these comments all apply to very small numbers as well, i.e. -2^53 – 1).
With ES11, the BigInt type is built in for these scenarios. You can define it by adding an n to the end of a number, as in mySafeBigNumber = 9007199254740992n
.
This is really a new type, not just some sleight of hand around the existing Number type. If you do typeof 12
, you get number
. If you do typeof mySafeBigNumber
, you get bigint
. Moreover, if you try anything nefarious like mySafeBigNumber - 12
, you will get an error: “Cannot mix BigInt and other types, use explicit conversions.”
To convert between the two, you can use the constructors. For example, you could write let myUnsfeBigNumber = Number(mySafeBigNumber)
, but wait — you shouldn’t do that because you just created an unsafely large Number object. So instead, only convert when the BigInt has been reduced to smaller than the MAX_SAFE_INTEGER value.
In a similar vein, it’s worth pointing out that comparing Numbers and BigInts for equality will always return false, since they are different types.
BigInt also supports representation in binary, oct, and hex notation and supports all the typical math operators you might expect except the unary plus operator, whose purpose is to convert a value to a Number. The reason for this is rather obscure, in avoiding breaking changes to existing non-JS asm code.
Finally, it might seem redundant to point out, since it’s in the very name, but BigInt represents integers. Running the code x = 3n; x = x / 2n;
will result in a value of 1n for x. BigInts quietly dispose of the fractional part of numbers.
Nullish coalescing
Nullish coalescing is the most poetically named of the ES11 features, and joins optional chaining in aiding us in our dealings with nullish values. Nullish coalescing is also a new symbol, the double question mark: ??
. It is a logical operator with similar behavior to the logical OR operator (the double pipe symbol ||
).
The difference between ??
and ||
is in how the operator handles nullish versus falsy values. Most JavaScript developers are familiar with how the language treats non-boolean values when testing them as true/false (in short, false, 0, null, empty strings, and undefined are considered false and everything else resolves to true; more details here). We’ll often take advantage of this to test for the existence of a thing and if it doesn’t exist, then use something else, like so:
let meaningOfLife = answer || 42;
This allows for setting a kind of default while quickly testing for the existence of something in answer
. That works great if we really want the meaningOfLife
to default to 42 if any of the falsy values are set on answer
. But what if you only want to fall back to 42 if there is an actual nullish value (null or undefined, specifically)?
??
makes that simple. You use
let meaningOfLife = answer ?? 42;
To make this clear, think about setting 0 as the value on answer
, and how assigning a value to meaningOfLife
would work using ||
vs ??
as in Listing 2. In this case, we want to keep 0 as the value if it’s set, but use 42 if answer
is actually empty.
Listing 2. Nullish coalescing in action
let answer = 0;let meaningOfLife = answer ?? 42;
// meaningOfLife === 0 - what we wantlet meaningOfLife = answer || 42;
// meaningOfLife === 42 - not what we want let answer = undefined;let meaningOfLife = answer ?? 42;
// meaningOfLife === 42 - what we wantlet meaningOfLife = answer || 42;
// meaningOfLife === 42 - also what we want
String.prototype.matchAll
The ES11 spec adds a new method to the String prototype: matchAll
. This method applies a regular expression to the String instance and returns an iterator with all the hits. For example, say you wanted to scan a string for all the places where a word started with t or T. You can do that as in Listing 3.
Listing 3. Using matchAll
let text = "The best time to plant a tree was 20 years ago. The second best time is now.";
let regex = /(?:^|s)(t[a-z0-9]w*)/gi; // matches words starting with t, case insensitive
let result = text.matchAll(regex);
for (match of result) {
console.log(match[1]);
}
Set aside the inherent density of regex syntax and accept that the regular expression defined in Listing 3 will find words starting with t or T. matchAll()
applies that regex to the string and gives you back an iterator that lets you simply and easily walk over the results and access the matching groups.
Dynamic imports
ES11 introduces an advancement in how you can import modules, by allowing for arbitrary placement of imports that are loaded asynchronously. This is also known as code splitting, something we have been doing via build tools for years now. Dynamic imports is another example of the spec catching up with in-the-wild practices.
A simple example of the syntax is shown in Listing 4.
Listing 4. Async module import
let asyncModule = await import('/lib/my-module.ts');
This kind of import can appear anywhere in your JS code, including as a response to user events. It makes it easy to lazy-load modules only when they are actually needed.
Promise.allSettled()
The promise.allSettled()
method allows you to observe the results of a set of promises whether they are fulfilled or rejected. This can be contrasted to promise.all()
, which will end with a rejected promise or error non-promise. promise.allSettled()
returns an array of objects describing the results of each promise.
This is useful when watching a group of unrelated promises. That is, you want to know what happened to them all, even if some in the middle fail. Listing 5 has an example.
Listing 5. promise.allSettled() example
let promise1 = Promise.resolve("OK");
let promise2 = Promise.reject("Not OK");
let promise3 = Promise.resolve("After not ok");
Promise.allSettled([promise1, promise2, promise3])
.then((results) => console.log(results))
.catch((err) => console.log("error: " + err));
The catch lambda does not fire in this case (it would if we had used promise.all
). Instead, the then
clause executes, and an array with the contents of Listing 6 is returned. The key takeaway here is that the third promise has run and we can see its outcome even though promise2
failed before it.
Listing 6. promise.allSettled() results
[
{"status":"fulfilled","value":"OK"},
{"status":"rejected","reason":"Not OK"},
{"status":"fulfilled","value":"After not ok"}
]
Export star syntax
This feature adds the ability to export *
from a module. You could already import *
from another module, but now you can export with the same syntax, like Listing 7.
Listing 7. export * example
Export * from '/dir/another-module.js'
This is a kind of module chaining, allowing you to export everything from another module from inside the current module. Useful if you are building a module that unites other modules into its API, for example.
Standardization of for-in ordering
Did you know that the order of enumeration over collections in for-in loops in JavaScript was not guaranteed? Actually, it was guaranteed by all JavaScript environments (browsers, Node.js, etc.), but now this de facto standard has been absorbed by the spec. Theory imitates practice again.
An evolving spec
One of the most interesting things about working in the software industry is watching the evolution of things. Programming languages are one of the most fundamental expressions of all, where much of the philosophy of computer science meets the reality of day-to-day coding. JavaScript (like other living languages) continues to grow in response to those forces with its yearly release schedule.