Another Year
Another year has passed, and we now know the latest features that have been finalised to go into ES2020, the latest specification of JavaScript/ECMAScript. The finalised proposals can be found here on the TC39 proposals GitHub repository. You may be wondering why or how these proposals are put together, and the journey of a JavaScript proposal before it goes into the language itself. Well, you are in luck.
How Features are added to JavaScript
No, it's not Brendan Eich, or Google, or some supreme being. The JavaScript specification is managed and iterated by a committee called TC39 (Technical Committee 39). TC39 is made up of various developers, people from academia and platform enthusiasts.
TC39 meet around 6 times a year, mostly in the US but also in Europe. They work with the community to accept proposals for new JavaScript features and take them through the 4 "stages" of a JavaScript language proposal. The 4 stages are as follows:
-
Stage 0: Strawperson
You have made a proposal to the language. This is generally done by raising a PR against the TC39 ECMAScript GitHub repository
-
Stage 1: Proposal
A TC39 member has been identified as a "champion" who is on board with the idea of introducing your feature. A public GitHub repository is created around your proposal, with examples, high-level API, rationale and potential issues.
-
Stage 2: Draft
Things are starting to get real. The Draft stage now means that all of the syntax and semantics of your proposal need to be nailed down. This involves describing your proposal feature using the formal specification language you will see in the JavaScript spec itself.
-
Stage 3: Candidate
Stage 3 is when your proposal is pretty much ready to go - TC39 reviewers have signed off on the specification text. Responsibility falls to the community at this stage. Developers are expected to use the feature and provide feedback that is only possible through actually using it in their software. The only changes made to your proposal here will be critical bugs and problems that are uncovered by the community.
-
Stage 4: Finished
Done and dusted. Your proposal has been well tested in the community in real implementations. Your proposal will be included in the next version of the ECMAScript standard and will be used by millions.
In this blog post, we are going to dive into each of the confirmed, stage 4 features that are being added to ES2020, as well as some examples.
String.prototype.matchAll
String.prototype.matchAll
is a utility function that's used to get all the matches for a particular regular expression (including capturing groups, which will be explained later). How was this problem being solved before ES2020? Let's take a simple example and iterate. Say we have the following string:
const test = "climbing, oranges, jumping, flying, carrot";
We want to get the verb from each of the words that end with ing
. For example climb
from "climbing" and jump
from "jumping". This is a perfect job for regex. Lets walk through the steps.
- Search the string for any words that end in "ing" (eg. "climbing")
- Capture all the letters that come before "ing" in the word (eg. "climb")
- Return them
Okay, so to do that we might use the following regex.
const regex = /([a-z]*)ing/g;
Regex is hard. Let's break it down so we understand how this works.
([a-z]*)
- match any string which contains the letters a-z in a row. We wrap this in parens()
to make it a "capturing group". A capturing group is as the name suggests - we want to "capture" the group of characters that match this particular part. In our example, we want to match all words ending in "ing", but what we really want are the letters before that, hence using a capturing group.ing
- only match strings that end in "ing"/g
- global. Search the whole input string. Don't stop at the first match.
String.prototype.match
Let's use our regex to find our verbs. One option in JavaScript is to use the match
function on a string, which lets you pass in a regex expression.
const test = "climbing, oranges, jumping, flying, carrot";
const regex = /([a-z]*)ing/g;
test.match(regex);
// ["climbing", "jumping", "flying"]
No good. It's returning the full words rather than just the verbs! This is happening because match
does not support capturing groups with the /g
flag - meaning that we can't use this to solve our problem. match
would be fine if we didn't need to use capturing groups, but in this case we do. Let's try something else.
RegExp.prototype.exec
The exec
method is executed on a regex itself, rather than the string like match
. exec
has support for capturing groups, but is a slightly more clunky API to use. You must keep calling exec
over and over on the regex to get the next match. This requires us to create an infinite loop and continue calling exec
until there are no matches left.
const regex = /([a-z]*)ing/g;
const matches = [];
while (true) {
const match = regex.exec(test);
if (match === null) break;
matches.push(match[1]);
}
matches
// ["climb", "jump", "fly"]
This approach works fine, but it is a little confusing and unintuitive. There are two primary reasons for this:
- It only performs the intended operation if the
/g
flag is set at the end. This can get a little confusing if you are passing the regex around as a variable or parameter. - When using the
/g
flag,RegExp
objects are stateful and store a reference to their last match. This can cause strange bugs if you are re-using the same regex over and over with multiple calls toexec()
.
Using String.prototype.matchAll
Finally - we have arrived. (If you skipped to this part, I don't judge you.) String.prototype.matchAll
will make our lives much easier here and provide a simple solution that supports capturing groups, returning an iterable that we can spread into an array. Let's refactor our code above to use matchAll
.
const test = "climbing, oranges, jumping, flying, carrot";
const regex = /([a-z]*)ing/g;
const matches = [...test.matchAll(regex)];
const result = matches.map(match => match[1]);
result
// ["climb", "jump", "fly"]
We get a 2 dimensional array with the full word match in the first element ("climbing") as well as the captured group in the second element ("climb"). By iterating over and pulling out the second element, we get the results we want. Great!
Dynamic import()
This is one you may be already familiar with. It's been supported by webpack since v2 and is common in production JavaScript applications for "code splitting". Code splitting is very powerful in a single page application - in a lot of cases speeding up initial page load times significantly.
The dynamic import syntax allows us to call import
as a function that returns a promise. This becomes useful for dynamically loading modules at runtime. For example, you may want to load a certain component or module based on some logic in your code.
// JavaScript for side panel is loaded
const sidePanel = await import("components/SidePanel");
sidePanel.open();
Interpolation is also supported.
async function openSidePanel(type = "desktop") {
// JavaScript for desktop side panel is loaded
const sidePanel = await import(`components/${type}/SidePanel`);
sidePanel.open();
}
This feature enhances the performance of our applications. We don't have to load all the JavaScript up front. Dynamic imports give us the control to only load exactly as much JS as we need to.
BigInt
The largest number JavaScript can handle is 2^53
. That's 9007199254740991
, or you can use the slightly more memorable Number.MAX_SAFE_INTEGER
.
What happens when you go beyond MAX_SAFE_INTEGER
? Well, it's not so SAFE
anymore.
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 - wut
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994 - WUT
The BigInt
type in ES2020 solves this. To convert a number literal to a BigInt
, you can use the BigInt
constructor, or you simply add a n
to the end of it. So to fix our example above where we got the same value after adding 2 to Number.MAX_SAFE_INTEGER
:
BigInt(Number.MAX_SAFE_INTEGER) + 2n; // 9007199254740993n ✅
Who needs these numbers?
You might be surprised to hear that it's quite common to have numbers this large in software development. Timestamps and unique identifiers can be numbers this large.
For example, Twitter use integers this large as unique keys for tweets. You would see weird bugs in your JavaScript application if you tried to store these as numbers without BigInt
. You would have to use a community package, or store them as a string instead - which is a common solution that JavaScript developers were using to solve this problem in environments where BigInt
isn't supported.
Promise.allSettled
Let's say you are sitting an exam. When you receive your results, you find out you got 99% of the questions correct. In most walks of life, you would have passed with flying colours. In this case, though - you receive a big red stamp on your results letter telling you that you failed.
This is how Promise.all works. Promise.all
takes an array of promises, and concurrently fetches their results. If they all succeed, your Promise.all
succeeds. If one or more fail, your promise rejects. In some cases you may want this behaviour - but not always.
Enter Promise.allSettled
Promise.allSettled
of ES2020 is much kinder when it comes to your exam. It will give you a pat on the back and tell you not to worry about that 1% of promises that failed.
A promise is regarded as "settled" when it comes back - pass or fail. Promise.allSettled
allows us to pass an array of promises and it will resolve when they are all settled. The return value of the promise is the array of results. Let's look at an example.
const promises = [
fetch('/api1'),
fetch('/api2'),
fetch('/api3'),
];
Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));
// "fulfilled"
// "fulfilled"
// "rejected"
globalThis
We live in a world where "universal" JavaScript is common. This means that the same JavaScript code could be running on the client and on the server in NodeJS. This presents a particular set of challenges.
One is the global object, accessible from any piece of running code. This is window
in the browser, but global
in Node. Writing universal code that accesses this global object relies on some conditional logic, that may look something like this (cover your eyes).
(typeof window !== "undefined"
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
Thankfully, ES2020 brings with it the addition of the globalThis
global variable. This will do the heavy lifting above for you and means you can now relax when it comes to accessing window
or global
in either front-end or back-end code.
globalThis.something = "Hello"; // Works in Browser and Node.
for-in mechanics
for (x in obj) ...
is a super useful syntax for many things - mainly iterating over the keys of an object.
for (let key in obj) {
console.log(key);
}
This proposal is related to the order and semantics of which elements are iterated in a for..in
loop. Before this proposal, most JavaScript engines had already applied common sense - currently all major browsers loop over the properties of an object in the order in which they were defined. There were some nuances, however. These mainly involved more advanced features like proxies. for..in
loop semantics have historically been left out the JavaScript spec, but this proposal ensures everyone has a consistent point of reference as to how for..in
should work.
Optional Chaining
Optional chaining is probably one of the most highly anticipated features to come to JavaScript in quite some time. In terms of impact on cleaner JavaScript code, this one scores very highly.
When checking for a property deep inside a nested object, you often have to check for the existence of intermediate objects. Let's work through an example.
const test = {
name: "foo",
age: 25,
address: {
number: 44,
street: "Sesame Street",
city: {
name: "Fake City",
lat: 40,
lon: 74
}
}
}
// when we want to check for the name of the city
if (test.address.city.name) {
console.log("City name exists!");
}
// City Name exists!
This works fine! But in software, we can't always rely on the happy path. Sometimes intermediate values will not exist. Let's look at the same example, but with no city
value defined.
const test = {
name: "foo",
age: 25,
address: {
number: 44,
street: "Sesame Street"
}
}
if (test.address.city.name) {
console.log("City name exists!");
}
// TypeError: Cannot read property 'name' of undefined
Our code is broken. This happens because we are trying to access name
on test.address.city
which is undefined
. When you attempt to read a property on undefined
, the above TypeError
will be thrown. How do we fix this? In a lot of JavaScript code, you will see the following solution.
const test = {
name: "foo",
age: 25,
address: {
number: 44,
street: "Sesame Street"
},
}
if (test.address && test.address.city && test.address.city.name) {
console.log("City name exists!");
}
// no TypeError thrown!
Our code now runs, but we've had to write quite a bit of code there to solve the problem. We can do better. The Optional Chaining operator of ES2020 allows you to check if a value exists deep inside an object using the new ?
syntax. Here is the above example rewritten using the optional chaining operator.
const test = {
name: "foo",
age: 25,
address: {
number: 44,
street: "Sesame Street"
},
}
// much cleaner.
if (test?.address?.city?.name) {
console.log("City name exists!");
}
// no TypeError thrown!
Looking good. We have condensed the long &&
chains into our much more succinct and readable optional chaining operator. If any of the values along the chain are null
or undefined
, the expression simply returns undefined
.
The optional chaining operator is very powerful. Have a look at the following examples to see other ways it can be used.
const nestedProp = obj?.['prop' + 'Name']; // computed properties
const result = obj.customMethod?.(); // functions
const arrayItem = arr?.[42]; // arrays
Nullish Coalescing Operator (null
or undefined
)
The Nullish Coalescing Operator is a very fancy sounding name for something very simple. What this feature gives us is the ability to check if a value is null
or undefined
and default to another value if so - nothing more, nothing less.
Why is this useful? Let's take a step back. There are five "falsy" values in JavaScript.
null
undefined
- empty string ("")
0
- Not a number -
NaN
We may have some code where we want to check against a numeric value. We want to assign a squad number to players in a team. If they already have a squad number, we keep that. Otherwise, we give them the value of "unassigned".
const person = {
name: "John",
age: 20,
squadNumber: 100
};
const squadNumber = person.squadNumber || "unassigned";
console.log(`${person.name}s squad number is ${squadNumber}`);
// "Johns squad number is 100"
This code works fine. However let's think about this from a slightly different angle. What if our person
had a slightly obscure squad number, like zero?
const person = {
name: "Dave",
age: 30,
squadNumber: 0
};
const squadNumber = person.squadNumber || "unassigned";
console.log(`${person.name}s squad number is ${squadNumber}`);
// "Daves squad number is unassigned"
This isn't right. Dave has been playing for the team for years. Our code has a bug. This happens because 0
is falsy, causing the false condition of our ||
to be invoked. This example is where the standard check for a value can fall short. You can of course, solve this by doing the following:
const person = {
name: "Dave",
age: 30,
squadNumber: 0
};
const squadNumber = person.squadNumber >= 0 ? person.squadNumber : "unassigned";
console.log(`${person.name}s squad number is ${squadNumber}`);
// "Daves squad number is 0"
Not a bad solution - but we can do better using the Nullish Coalescing operator (??
) to ensure that our value is null
or undefined
, before we say someones squad number is unassigned
.
const person = {
name: "Dave",
age: 30,
squadNumber: 0
};
// Nullish Coalescing Operator
// If person.squadNumber is null or undefined
// set squadNumber to unassigned
const squadNumber = person.squadNumber ?? "unassigned";
console.log(`${person.name}s squad number is ${squadNumber}`);
// "Daves squad number is 0"
Nothing wrong with that little bit more type safety and explicitness in our JavaScript.
import.meta
import.meta
is a convenience property that provides an object containing the base URL of the currently running module. If you are familiar with node, this functionality is available out of the box with CommonJS through the __dirname
or __filename
properties.
const fs = require("fs");
const path = require("path");
// resolves data.bin relative to the directory of this module
const bytes = fs.readFileSync(path.resolve(__dirname, "data.bin"));
What about the browser though? This is where import.meta
becomes useful. If you want to import a relative path from a JavaScript module running in the browser, you can use import.meta
to do so.
// Will import cool-image relative to where this module is running.
const response = await fetch(new URL("../cool-image.jpg", import.meta.url));
This feature is very useful for library authors - as they do not know how and where you will run your code.
Conclusion
All in all, the newest features added to the ECMAScript specification add even more utility, flexibility and power to the constantly evolving and developing ecosystem that is JavaScript. It's encouraging and exciting to see the community continue to thrive and improve at such a rapid pace.
You may be thinking - "That all sounds great.. but how do I get started using ES2020 features?"
When/how can I use this stuff?
You can use it now! In the later versions of most modern browsers and Node, you will have support for all of these features. caniuse.com is another great resource to check levels of compatibility for ES2020 features across browsers and node.
If you need to use these features on older browsers or versions of node, you will need babel/typescript.
Enabling ES2020 support
Using Babel 7.8.0 or TypeScript 3.7
Babel 7.8.0 and above as well as TypeScript 3.7 and above support ES2020 features out of the box.
Using babel plugins
If you can't upgrade your babel setup, you will have to install the ES2020 features through a multitude of plugins. Here's an example .babelrc config of how you may do that.
{
"plugins": [
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-bigint"
]
}
Feel free to reach out to me or follow me on Twitter, where I tweet and blog about JavaScript, Python, AWS, automation and no-code development.
I hope you learned something. Thanks for reading!