What's new in ECMAScript 2020
ECMAScript 2020, the 11th installment of our favorite programming language, contains a handful of new features. Some are small ones, but others have the potential to change forever the way we write JavaScript.
This article is an attempt at a short and concise overview of those new features. Grab your cup of tea and let's go.
Dynamic import()
ES2015 introduced the static import
syntax. Now you could export a variable from one module and then import it in another.
// utils.js
export function splitName(name) {
return name.split(" ");
}
// index.js
import { splitName } from "./utils";
console.log(splitName("John Snow"));
That syntax is called static because you can't import a module dynamically (depending on some conditions) at runtime. Note that it's not necessarily a bad thing: static imports can be optimized at compile time, allowing for Tree Shaking.
Dynamic imports, on the other hand, when used wisely, can help with reducing bundle size by loading dependencies on-demand.
The new dynamic import
syntax looks like a function (but it's not), and it returns a promise, which also means we can use async/await with it.
// ...
const mod = figure.kind === "rectangle" ? "rectangle.js" : "circle.js";
const { calcSquare } = await import(mod);
console.log(calcSquare(figure));
Nullish coalescing
The popular way of setting a default value with short-circuting has its flaws. Since it's not really checking for the emptiness, but rather checking for the falsyness, it breaks with values like false
, or 0
(both of which are considered to be falsy).
ES2020 introduces a new operator ??
which works similarly but only evaluates to the right-hand when the initial value is either null
or undefined
.
Here's a quick example:
const initialVal = 0;
// old way
const myVar = initialVal || 10; // => 10
// new way
const myVar = initialVal ?? 10; // => 0
I wrote a detailed article about this feature and how it compares to the other methods for setting a default value.
Optional chaining
The new optional chaining
operator aims to make the code shorter when dealing with nested objects and checking for possible undefineds
.
const user = { name: "John" };
// Fails with `Uncaught TypeError: Cannot read property 'city' of undefined`
const city = user.address.city;
// Works but verbose
let city = "Not Set";
if (user.address !== undefined && user.address !== null) {
city = user.address.city;
}
// Works and concise but requires a 3rd party library
const city = _.get(user, "address.city", "Not Set");
// 🤗
const city = user?.address?.city ?? "Not Set";
BigInt
BigInt
is a new object that represents numbers higher than Number.MAX_SAFE_INTEGER
(which is 2^53 - 1
). While for normal folks, it may sound more than enough, for some math applications and machine learning, the new BigInt
type comes in handy.
It comes with its own literal notation (just add an n
to a number):
const x = 9007199254740991n;
// or it can be constructed from a string
const y = BigInt("9007199254740991234");
BigInts
come with their own algebra, which is not translated to regular numbers by which we can't mix up numbers and BigInts. They should be coerced to either type first.
1 === 1n; // => false
1n + 1; // throws Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
6n << 3; // nope
6n << 3n; // that works
String.matchAll
So here's an example. Imagine, you have a very long string of text, and you need to extract all the tags (that are words starting with #
) out of it. Regular expressions to the rescue!
const tweet = "#JavaScript is full of #surprises. Both good and bad ones #TIL";
for (h of tweet.matchAll(/(#\w+)/g)) {
console.log(h[0]);
}
// or
const tags = [...tweet.matchAll(/(#\w+)/g)];
matchAll
returns an iterator. We could either iterate over it with for..of
, or we can convert it to an array.
Promise.allSettled
Remember the Promise.all function? It resolves only when all of the passed promises get resolved. It rejects if at least one of the promises got rejected, while the others may still be pending.
The new allSettled
behaves differently. It resolves whenever all of the promises finished working, that is, became either fulfilled or rejected. It resolves to an array that contains both the status of the promise and what it resolved to (or an error).
Thus, allSettled
is never rejected. It's either pending
, or resolved
.
A real-world problem might be removing a loading indicator:
// const urls = [...]
try {
await Promise.all(urls.map(fetch))
} catch (e) {
// at least one fetch is rejected here, but there may others still pending
// so it may be too early for removing the loading indicator
removeLoading()
}
// with allSettled
await Promise.allSettled(urls.map(fetch))
removeLoading()
globalThis
In JavaScript, there's always one big context object that contains everything. Traditionally, in browsers it was window
. But if you try accessing it in Node application, you'll get an error. There is no window
global object in Node; instead there is global
object. Then again, in WebWorkers, there is no access to window
, but there is self
instead.
The new globalThis
property abstracts away the difference. Meaning that you can always refer to globalThis
without caring in which context you are now.
Now, if you think that the naming is rather awkward, I'm totally with you, but note that naming it self
or global
could make some older code incompatible. So I guess we'll have to live with that.
What's next?
For your convenience, here are the links to the MDN documentation for each of the features mentioned in this article.
- Dynamic imports
- Nullish coalescing operator but also my own article
- Optional chaining
- BigInt
- Promise.allSettled
- globalThis
If you like articles like this, you can follow me on Twitter to get notified about the new ones.