ECMAScript 2022 (ES13) dropped on June 22, codifying the latest batch of new features for JavaScript. Every technology specification is a milestone in an ongoing dance with real-world usage. As developers use JavaScript, we continually discover opportunities and push the language into new territory. The ECMAScript specification responds by formalizing new features. These, in turn, establish a new baseline for JavaScript's continuing evolution.
The ES13 specification brings in eight new features for JavaScript. Let's get started with these new features that you can use today.
Class fields
Class fields is an umbrella proposal that encompasses several improvements for handling members on JavaScript classes: Class public and private instance fields, private instance methods and accessors, and static class features.
Public and private instance fields
Previously, the standard approach when declaring a member field inside the class
keyword was to introduce it in the constructor. The newest ECMAScript specification lets us define the member field inline as part of the class body. As shown in Listing 1, we can use a hashtag to denote a private field.
Listing 1. Inline public and private class fields
class Song {
title = "";
#artist = "";
constructor(title, artist){
this.title = title;
this.#artist = artist;
}
}
let song1 = new Song("Only a Song", "Van Morrison");
console.log(song1.title);
// outputs “Only a Song”
console.log(song1.artist);
// outputs undefined
In Listing 1, we define a class, Song
, using the class
keyword. This class has two members, title
and artist
. The artist
member is prefixed with a hash (#) symbol, so it is private. We allow for setting these fields in the constructor. Notice that the constructor must access this.#artist
with the hash prefix again; otherwise, it would overwrite the field with a public member.
Next, we define an instance of the Song
class, setting both fields via the constructor. We then output the fields to the console. The point is that song1.artist
is not visible to the outside world, and outputs undefined.
Note, also, that even song1.hasOwnProperty("artist")
will return false. Additionally, we cannot create private fields on the class later using assignment.
Overall, this is a nice addition, making for cleaner code. Most browsers have supported public and private instance fields for a while and it’s nice to see them officially incorporated.
Private instance methods and accessors
The hash symbol also works as a prefix on methods and accessors. The effect on visibility is exactly the same as it is with private instance fields. So, you could add a private setter and a public getter to the Song.artist
field, as shown in Listing 2.
Listing 2. Private instance methods and accessors
class Song {
title = "";
#artist = "";
constructor(title, artist){
this.title = title;
this.#artist = artist;
}
get getArtist() {
return this.#artist;
}
set #setArtist(artist) {
this.#artist = artist;
}
}
Static members
The class fields proposal also introduces static members. These look and work similarly to how they do in Java: if a member has the static
keyword modifier, it exists on the class instead of object instances. You could add a static member to the Song
class as shown in Listing 3.
Listing 3. Add a static member to a class
class Song {
static label = "Exile";
}
The field is then only accessible via the class name, Song.label
. Unlike Java, the JavaScript instances do not hold a reference to the shared static variable. Note that it is possible to have a static private field with static #label
; that is, a private static field.
RegExp match indices
Regex match
has been upgraded to include more information about the matching groups. For performance reasons, this information is only included if the /d
flag is added to the regular expression. (See the RegExp Match Indices for ECMAScript proposal for a deep dive on the meaning of /d
regex.)
Basically, using the /d
flag causes the regex engine to include the start and end of all matching substrings. When the flag is present, the indices
property on the exec
results will contain a two-dimensional array, where the first dimension represents the match and the second dimension represents the start and end of the match.
In the case of named groups, the indices will have a member called groups
, whose first dimension contains the name of the group. Consider Listing 4, which is taken from the proposal.
Listing 4. Regex group indices
const re1 = /a+(?<Z>z)?/d;
// block 1
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";
// block 2
m1.indices[1][0] === 4;
m1.indices[1][1] === 5;
s1.slice(...m1.indices[1]) === "z";
// block 3
m1.indices.groups["Z"][0] === 4;
m1.indices.groups["Z"][1] === 5;
s1.slice(...m1.indices.groups["Z"]) === "z";
// block 4
const m2 = re1.exec("xaaay");
m2.indices[1] === undefined;
m2.indices.groups["Z"] === undefined;
In Listing 4, we create a regular expression that matches the a
char one or more times, followed by a named matching group (named Z
) that matches the z
char zero or more times.
Code block 1 demonstrates that m1.indices[0][0]
and m1.indices[0][1]
contain 1 and 5, respectively. That is because the first match for the regex is the string from the first a
to the string ending in z
. Block 2 shows the same thing for the z
character.
Block 3 shows accessing the first dimension with the named group via m1.indices.groups
. There is one matched group, the final z
character, and it has a start of 4 and an end of 5.
Finally, block 4 demonstrates that unmatched indices and groups will return undefined.
The bottom line is that if you need access to the details of where groups are matched within a string, you can now use the regex match indices feature to get it.
Top-level await
The ECMAScript specification now includes the ability to package asynchronous modules. When you import a module wrapped in await
, the including module will not execute until all the await
s are fulfilled. This avoids potential race conditions when dealing with interdependent asynchronous module calls. See the top-level await proposal for details.
Listing 5 includes an example borrowed from the proposal.
Listing 5. Top-level await
// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);
// usage.mjs
import { output } from "./awaiting.mjs";
export function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
Notice in awaiting.mjs
the await
keyword in front of its use of dependent modules dynamic
and data
. This means that when usage.mjs
imports awaiting.mjs
, usage.mjs
will not be executed until awaiting.mjs
dependencies have finished loading.
Ergonomic brand checks for private fields
As developers, we want code that is comfortable—ergo ergonomic private fields. This new feature lets us check for the existence of a private field on a class without resorting to exception handling.
Listing 6 shows this new, ergonomic way to check for a private field from within a class, using the in
keyword.
Listing 6. Check for the existence of a private field
class Song {
#artist;
checkField(){
return #artist in this;
}
}
let foo = new Song();
foo.checkField(); // true
Listing 6 is contrived, but the idea is clear. When you find yourself needing to check a class for a private field, you can use the format: #fieldName in object
.
Negative indexing with .at()
Gone are the days of arr[arr.length -2]
. The .at method on built-in indexables now supports negative indices, as shown in Listing 7.
Listing 7. Negative index with .at()
let foo = [1,2,3,4,5];
foo.at(3); // == 3
hasOwn
hasOwn
Object.hasOwn is an improved version of Object.hasOwnProperty
. It works for some edge cases, like when an object is created with Object.create(null)
. Note that hasOwn
is a static method—it doesn’t exist on instances.
Listing 8. hasOwn() in action
let foo = Object.create(null);
foo.hasOwnProperty = function(){};
Object.hasOwnProperty(foo, 'hasOwnProperty'); // Error: Cannot convert object to primitive value
Object.hasOwn(foo, 'hasOwnProperty'); // true
Listing 8 shows that you can use Object.hasOwn
on the foo
instance created with Object.create(null)
.
Class static block
Here’s a chance for Java developers to say, Oh, we’ve had that since the '90s. ES 2022 introduces static initialization blocks to JavaScript. Essentially, you can use the static
keyword on a block of code that is run when the class is loaded, and it will have access to static members.
Listing 9 has a simple example of using a static block to initialize a static value.
Listing 9. Static block
class Foo {
static bar;
static {
this.bar = “test”;
}
}
Error cause
Last but not least, the Error
class now incorporates cause support. This allows for Java-like stack traces in error chains. The error constructor now allows for an options object that includes a cause
field, as shown in Listing 10.
Listing 10. Error cause
throw new Error('Error message', { cause: errorCause });