JavaScript evolves quickly. In 2021, several proposals have moved to Stage 4 of the TC39 process and will be included in ES2022. They add the following features to JavaScript:
Classes and Instances
- Private instance fields, methods, and accessors
- Existence checks for private fields
- Public static class fields
- Private static class fields and methods
- Static class initialization blocks
Module Loading
- Top-Level
await
Built-in Objects
- Error:
.cause
- Array, String, and TypedArray:
.at()
- Object:
.hasOwn()
- RegExp: match
.indices
('d
' flag)
This blog post describes each feature, shows an example of how it can be used, and looks at current browser and Node.js support (as of December 2021). Let's get started:
Private Instance Fields, Methods, and Accessors
Encapsulation is one of the core principles of object-oriented programming. It is usually implemented using visibility modifiers such as private
or public
.
The private instance fields, methods, and accessors features [1, 2] add hard visibility limitations to JavaScript. The #
prefix marks a field, method, or accessor in a class as private, meaning that you cannot access it from outside the instances themselves.
Here is an example of a private field and method; accessors work similarly:
class Example {
#value;
constructor(value) {
this.#value = value;
}
#calc() {
return this.#value * 10;
}
print() {
console.log(this.#calc());
}
}
const object = new Example(5);
console.log(object.#value); // SyntaxError
console.log(object.#calc()); // SyntaxError
object.print(); // 50
Most browsers (Dec 2021 usage: ~90%) and Node.js 12+ support private instance fields. The support for private methods and accessors is more limited in browsers (Dec 2021 usage: ~80%). Node.js has supported the feature since version 14.6. You can transpile your code with Babel to use private class fields and methods on environments that don't directly support them.
Existence Checks For Private Fields
Since trying to access a non-existing private field on an object throws an exception, it needs to be possible to check if an object has a given private field. The in
operator can be used to check if a private field is available on an object:
class Example {
#field
static isExampleInstance(object) {
return #field in object;
}
}
The browser support for using the in
operator on private fields is limited (Dec 2021 usage: ~70%). Node.js supports the feature since version 16.4. You can transpile usages of the in
operator for private fields with Babel.
Public Static Class Fields
Static class fields are a convenient notation for adding properties to the class object.
// without static class fields:
class Customer {
// ...
}
Customer.idCounter = 1;
// with static class fields:
class Customer {
static idCounter = 1;
// ...
}
Most browsers (Dec 2021 usage: ~90%) and Node.js 12+ support public class fields.
Private Static Class Fields and Methods
Similar to private instance fields and methods, encapsulation and visibility limitations are helpful on the class level. The private static methods and fields feature adds hard visibility limitations for class-level fields and methods using the #
prefix.
class Customer {
static #idCounter = 1; // static private
static #getNextId() { // static private
return Customer.#idCounter++;
}
#id; // instance private
constructor() {
this.#id = Customer.#getNextId();
}
toString() {
return `c${this.#id}`;
}
}
const customers = [new Customer(), new Customer()];
console.log(customers.join(' ')); // c1 c2
The browser and Node.js support are similar to the private instance fields and methods above.
Static Class Initialization Blocks
Sometimes it is necessary or convenient to do more complex initialization work for static class fields. For the private static fields feature from above, this initialization must even happen within the class because the private fields are not accessible otherwise.
The static initializer blocks feature provides a mechanism to execute code during the class definition evaluation. The code in a block statement with the static
keyword is executed when the class is initialized:
class Example {
static propertyA;
static #propertyB; // private
static { // static initializer block
try {
const json = JSON.parse(fs.readFileSync('example.json', 'utf8'));
this.propertyA = json.someProperty;
this.#propertyB = json.anotherProperty;
} catch (error) {
this.propertyA = 'default1';
this.#propertyB = 'default2';
}
}
static print() {
console.log(Example.propertyA);
console.log(Example.#propertyB);
}
}
Example.print();
The browser support for static class initialization blocks is limited (Dec 2021: ~70%). Node.js supports the feature since version 16.4. You can transpile code with static initializer blocks with Babel.
Top-Level Await
Async functions and the await
keyword were introduced in ES2017 to simplify working with promises. However, await
could only be used inside async
functions.
The top-level await
feature for ES modules makes it easy to use await
in CLI scripts (e.g., with .mjs
sources and zx), and for dynamic imports and data loading. It extends the await
functionality into the module loader, which means that dependent modules will wait for async modules (with top-level await
) to be loaded.
Here is an example:
// load-attribute.mjs
// with top-level await
const data = await (await fetch("https://some.url")).text();
export const attribute = JSON.parse(data).someAttribute;
// main.mjs
// loaded after load-attribute.mjs is fully loaded
// and its exports are available
import { attribute } from "./load-attribute.mjs";
console.log(attribute);
Top-level await is supported on modern browsers (Dec 2021 usage: ~80%) and Node.js 14.8+. It is only available for ES modules, and it is doubtful that CommonJS modules will ever get top-level await support. Code with top-level await
can be transpiled during the bundling phase to support older browsers, such as Webpack 5 experiments.topLevelAwait = true
.
Error: .cause
Errors are often wrapped to provide meaningful messages and record the error context. However, this means that the original error can get lost. Attaching the original error to the wrapping error is desirable for logging and debugging purposes.
The error cause feature provides a standardized way to attach the original error to a wrapping error. It adds the cause
option to the Error
constructor and a cause
field for retrieving the original error.
const load = async (userId) => {
try {
return await fetch(`https://service/api/user/${userId}`);
} catch (error) {
throw new Error(
`Loading data for user with id ${userId} failed`,
{ cause: error }
);
}
}
try {
const userData = await load(3);
// ...
} catch (error) {
console.log(error); // Error: Loading data for user with id 3 failed
console.log(error.cause); // TypeError: Failed to fetch
}
The current browser support for the error clause feature is limited (Dec 2021 usage: ~70%). Node.js supports the feature since version 16.9. You can use the error cause polyfill to start using the feature today, even in JS environments where it is not supported.
Array, String, and TypedArray: .at()
Getting elements from the end of an array or string usually involves subtracting from array's length, for example, let lastElement = anArray[anArray.length - 1]
. This requires that the array is stored in a temporary variable and prevents seamless chaining.
The .at() feature provides a way to get an element from the beginning (positive index) or the end (negative index) of a string or an array without a temporary variable.
const getExampleValue = () => 'abcdefghi';
console.log(getExampleValue().at(2)); // c
console.log(getExampleValue()[2]); // c
const temp = getExampleValue();
console.log(temp[temp.length - 2]); // h
console.log(getExampleValue().at(-2)); // h - no temp var needed
The browser support for the .at feature is currently limited (Dec 2021 usage: ~70%), and it is only available in Node.js 16.6+. You can use the .at() polyfill from Core JS in the meantime.
Object: .hasOwn()
The Object.hasOwn feature is a more concise and robust way of checking if a property is directly set on an object. It is a preferred alternative to using hasOwnProperty
:
const example = {
property: '123'
};
console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property')); // preferred
The browser support is currently limited (Dec 2021 usage: ~70%), and you need Node 16.9+ to use hasOwn
directly. In the meantime there is a Core JS polyfill for hasOwn
.
RegExp: Match Indices ('d' Flag)
By default, regular expression matches record the start index of the matched text, but not its end index and not the start and end indices of its capture groups. For use cases such as text editor syntax or search result highlighting, having capture group match indices as part of a regular expression match can be helpful.
With the regexp match indices feature ('d' flag), the match and capture group indices are available in the indices
array property of the regular expression result.
The matched text position and the match indices position are the same, e.g., the full matched text is the first value in the match array and the indices array. The indices of the named captured groups are recorded in indices.groups
.
Here is an example:
const text = "Let's match one:1.";
const regexp = /match\s(?<word>\w+):(?<digit>\d)/gd;
for (const match of text.matchAll(regexp)) {
console.log(match);
}
The above example code has the following output:
[
'match one:1',
'one',
'1',
index: 6,
input: "Let's match one:1.",
groups: { word: 'one', digit: '1' },
indices: {
0: [6,17],
1: [12,15],
2: [16,17],
groups: {
digit: [16, 17],
word: [12, 15]
}
}
]
The browser support for the RegExp match indices feature is currently limited (Dec 2021 usage: ~80%). In Node.js, you can activate the feature with the --harmony-regexp-match-indices
flag, but it is disabled by default. You can use the RegExp match indices polyfill in the meantime.
Conclusion
The new JavaScript from 2021 features help make development more convenient and robust, and most of them already work on the latest browsers and Node.js environments.
However, many users are still on browsers and environments without full ES2022 support. For production use, it is essential to check the target environments and use polyfilling and transpiling as needed or to wait a bit longer before using the new features.
Happy coding in 2022!
Top comments (2)
Thanks for writing up this summary.
One minor improvement: the article currently says that both...
...are not supported in Node. These features shipped in V8 9.1 and then landed in Node 16.4.
Also, Error.cause shipped in V8 9.3 and then landed in Node 16.9.
Thanks for the correction. I have updated the article. I based my research on the data from MDN, believing it is accurate.