What’s new in ECMAScript 2020?
Once a year, we’re treated to a new batch of features in ECMAScript, better known as JavaScript. In this post, I’ll give a quick overview of the history of ECMAScript (including how and why it technically differs from JavaScript) and its feature update process, and talk about a few of the new features released in ECMAScript 2020.
A brief history of JavaScript
The JavaScript we know and love today was born in the mid-1990s, in the midst of a battle between two browsers.
In 1995, Netscape (which dominated the browser market at the time) partnered with Sun Microsystems to develop a simple scripting language that would empower designers and entry-level developers to create rich client-side interactions. They called this new language JavaScript. Shortly thereafter, Microsoft launched the first version of their Internet Explorer browser with their own scripting language, which they named — confusingly — JScript.
As you might imagine, the parallel development of JavaScript and JScript created serious compatibility issues. It was difficult, if not impossible, to write code that would run on both Netscape and Internet Explorer, and as a result, the web ecosystem quickly became fragmented. In 1996, in an effort to address these problems, the code for JavaScript was submitted to international standards organization Ecma so that a consistent cross-browser standard could be developed.
Ecma formed a committee (TC-39) to make key decisions about what would be included in the new, standardized language. At the time, that committee included representatives from both Netscape and Microsoft. Today, TC-39 includes representatives from all major browser manufacturers (Google, Apple, Mozilla, Microsoft, etc.) and still meets regularly to consider additions and updates to the language.
Officially, what most of us know as JavaScript is really the standardized “ECMAScript” that was first established in 1996. The name JavaScript is technically owned by Oracle, which acquired Sun Microsystems in 2010 — but the distinction is fairly inconsequential at this point, and the two names are generally used interchangeably.
How features are added to ECMAScript
The Ecma TC-39 committee has a four-stage process for considering and confirming updates to the language:
- Stage One: Proposal. At this stage, a developer submits a detailed explanation of the feature concept, usually including demos. The committee considers the proposal and makes a decision to move forward or reject the idea.
- Stage Two: Draft. A draft is understood to be a formal description of the problem the new feature solves, how it solves it, and exactly what the scope of the update will be. A feature that makes it to the draft stage is likely to become part of the language, and the scope is usually locked (no major changes) at this point.
- Stage Three: Candidate. The feature specs are mostly finalized, the feature is subjected to tests, and feedback is collected from implementers and users.
- Stage Four: Finished. The feature has passed tests and is ready to be included in the standard.
Each year, the TC-39 -that is, the technical committee in charge of standardizing the Javascript Language- releases a new version of the ECMAScript specification with new language feature. And this year’s ES2020 brings some very useful features that will make our code better optimized, cleaner and more readable.
Nullish coalescing operator
Provides an easy method to set a default value for object properties equal to null or undefined.
Let’s imagine we have an object with some of its properties null or undefined:
Here’s an example:
const config = { const reminder = { title: 'Feed the dog', body: '', shouldRingAlarm: false, canSnoozeAfterSeconds: 0, category: null };
Just until now, a common expression to assign default values was to use || (the OR operator) like this:
const title = reminder.title || 'New reminder'; // 'Feed the dog' const body = reminder.body || 'Set reminder body'; // 'Set reminder body'
This form might be ok if we are checking against empty, or rather, falsy values. But what would happen if we want our reminder’s body to remain empty and only take the default value when the property is null or omitted?
Since the value of reminder.body evaluates to false, it would take its default value ‘Set reminder body’, which might not be our expected output.
We then might be forced to evaluate the property like this:
const body = (reminder.body === null || reminder.body === undefined) ? 'Set reminder body' : reminder.body; // ''
Which is just a lot of code.
This is where the Nullish Coalescing operator (??) comes in handy. Instead of checking for falsy values, the operator checks if the left-hand side expression evaluates to null or undefined, and if it does, it returns whatever is on the right-hand side.
const body = reminder.body ?? 'Set reminder body'; // ''
Now we can be sure that falsy properties won’t be replaced by its default value.
Here is a comparison between || and ?? using the rest of the properties.
// Using OR operator const title = reminder.title || 'New reminder'; // 'Feed the dog' const body = reminder.body || 'Set reminder body'; // 'Set reminder body' const shouldRingAlarm = reminder.shouldRingAlarm || true; // true const alarmHour = reminder.alarmHour || '12:00'; // '12:00' (since reminder.alarmHour is undefined) const canSnoozeAfterSeconds = reminder.canSnoozeAfterSeconds || 3; // 3 const category = reminder.category || 'Personal'; // 'Personal' // Using Nullish coalescing operator const title = reminder.title ?? 'New reminder'; // 'Feed the dog' const body = reminder.body ?? 'Set reminder body'; // '' const shouldRingAlarm = reminder.shouldRingAlarm ?? true; // false const alarmHour = reminder.alarmHour ?? '12:00'; // '12:00' (since reminder.alarmHour is undefined) const canSnoozeAfterSeconds = reminder.canSnoozeAfterSeconds ?? 3; // 0 const category = reminder.category ?? 'Personal'; // 'Personal'
Optional chaining operator
How many times have you had to access a property that is nested several levels deep inside an object?
If you have consumed an API returning a JSON type response, chances are that you’ve already encountered yourself in this scenario multiple times before. Then you know that, in order to access the desired property, you first have to check for the existence of intermediate levels in the object before going deeper.
Let the following object be our API’s JSON response:
const user = { profile: { name: 'Thomas A. Anderson', alias: ['Neo', 'The One', 'Mr. Anderson'], birthPlace: { reality: 'The Matrix', simulation: 'New York' } }, abilities: { fight() { console.log('I know Kung Fu.'); } } };
If we want to display the user’s name, we first need to check if the property profile exists inside our user object and is not null:
const name = user.profile && user.profile.name;
This can get pretty verbose for properties deep nested:
const realBirthPlace = user.profile && user.profile.birthPlace && user.profile.birthPlace.reality;
Luckily, we can now use Optional Chaining to avoid repeating code.
const name = user.profile?.name; // 'Thomas A. Anderson' const simulatedBirthPlace = user.profile?.birthPlace?.simulation; // 'New York'
The ?. operator will return the desired property only when the left-hand side operand results different from undefined or a null value. Otherwise, the whole expression will result in undefined.
const user = { profile: {...}, ship: { crew: null } }; const ship = user.ship?.name; // undefined const captain = user.ship?.crew?.captain; // undefined
We can use this form not only with static properties, but with dynamic properties (such as arrays), and with functions as well:
const firstAlias = user.profile?.alias?.[0]; // Neo const fight = user.abilities?.fight?.(); // I know Kung Fu. const fly = user.abilities?.fly?.() // undefined
As a side note, if you’re wondering why the operator ?. was chosen instead of just ?, it’s because it was easier for the parser to distinguish from the conditional operator (a ? b : c).
Promise.allSettled
Promise also got a new combinator method for handling multiple promises
Let’s imagine we are fetching some info from multiple API endpoints using promises. We want to show a spinner for as long as the promises are not finished and remove it once they are done. We don’t care about their status of success or error, we only need to know if they have all completed – or settled.
With Promise.allSettled we can input an array of Promises and get as a result a single promise fulfilled with an array of the correspondent results of the input promises, once all of the input promises are settled.
const promises = [ fetch('/api-call-1'), fetch('/api-call-2'), fetch('/api-call-3'), ]; Promise.allSettled(promises).then(() => { // All API calls have finished (either failed or succeeded). removeLoadingIndicator(); });
This new method comes to add a new case to previously existing combinators:
Promise.allSettled | Waits for all the promises to resolve. |
Promise.all | Waits for all the promises to resolve or once a promise is rejected. |
Promise.race | Waits for one of the promises to settle. |
import()
Now it is possible to dynamically import Javascript modules in a function-like syntax. A dynamic import can help our application reduce the time it takes to load, as you would be importing the module at runtime, just at the exact moment it is needed.
We can use the syntax import(specifier) anywhere in the code to returns a promise which resolves to the requested module. The module is then fetched, instantiated, and all of its dependencies evaluated, as would occur with a static import.
const modulePath = "./myModule.js"; import(modulePath) .then(module => { module.doSomething(); });
Another benefit is that the specifier does not need to be a string literal, but it will also allow the use of string templates.
const version = '1.0.0';
import(`./module-${version}.js`);
On the other hand, there are some downsides of using dynamic import, such as that we would no longer be able to statically analyze the code, nor it can be optimized by tree shaking.
globalThis
The globalThis property provides a standard way of accessing the global this value (and hence the global object itself) across environments.
Unlike similar properties such as window and self, globalThisis guaranteed to work in window (browsers) and non-window (scripts) contexts. In this way, you can access the global object in a consistent manner without having to know which environment the code is being run in.
var getGlobal = function () { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); };
globalThis aims to consolidate the increasingly fragmented ways of accessing the global object by defining a standard global property.
When will ES2020 be available?
According to the site caniuse.com you can use these features right away if you are using the following browsers:
Feature | Chrome | Firefox | Safari | Edge |
?? | 80 | 74 | – | 80 |
?. | 80 | 74 | – | 80 |
Promise.allSettled | 80 | 74 | 13 | 80 |
import() | 80 | 74 | 13 | 80 |
globalThis | 80 | 74 | 13 | 80 |
Or by using the Babel preset: babel-preset-es2020. Already included in Babel 7.8.0