JavaScript as a language is changing every year now. Starting with the major changes brought into the language in 2015 with ES6 or ES2015, the language design team has been putting efforts to add new features to the language every year. The TC39 team proposes or reviews the features submitted based on the usage of JavaScript across different platforms and these features are designed to make the language more relevant. Some of these features add new syntax or improve the existing features, and some of them add new APIs.
Any new features to JavaScript must go through a process with the TC39 team. The proposals made to the TC39 team go through 4 stages of evaluations and when a proposal reaches stage 4, it is going to make it to the language specifications. The proposals at different stages can be found in the proposals repository. The proposals that reach stage 4 and are considered to be included in the coming version of the language can be found in the finished proposals.
The features included in ECMAScript 2022 add new features to the classes, allow modules to be asynchronous, improve to regular expressions, make custom errors more meaningful and add new capabilities to objects and arrays. Let’s dive in and explore these features now.
New features in ES 2022
The list of features fir ES 2022 can be found in the list of finished proposals. Let’s look into these features. The table in the link also contains the list of features that could be included in ES 2023, we will look at those features in another article in the future.
Accessible Object.prototype.hasOwnProperty
JavaScript adds the method hasOwnProperty to all the objects inherited from Object, that can be used to check if the object contains a property. We can perform the following to check if an object has a property:
let obj = {name: 'Ravi'};
console.log(obj.hasOwnProperty('name')); // true
The downside of using hasOwnProperty is, it doesn’t work the way it is supposed to in certain cases. It is possible to create a custom property or a method in the object named as hasOwnProperty, as shown here:
let obj = { hasOwnProperty: false };
console.log(obj.hasOwnProperty('name')); // throws error
In such cases the method can be used directly from Object.prototype to get the desired behavior, as shown below:
let obj = { hasOwnProperty: false };
console.log(Object.prototype.hasOwnProperty.call(obj, 'name'));
// false
The proposal adds the method hasOwn to the Object that provides the same behavior. The method can be accessed directly from Object without the need of navigating to prototype and accessing it. Following are some examples using this method:
let obj = {name: 'Ravi'};
console.log(Object.hasOwn(obj, 'name')); // true
let obj2 = { hasOwnProperty: false };
console.log(Object.hasOwn(obj2, 'name')); // false
let obj3 = Object.create({name: 'Ravi'});
obj3.age = 22;
console.log(Object.hasOwn(obj3, 'name'), Object.hasOwn(obj3, 'age'));
// false true
As shown in the case of obj3 in the above snippet, Object.hasOwn considers only direct properties and it doesn’t consider the inherited properties.
The .at() Proposal
Today JavaScript doesn’t provide a way to apply negative indices on arrays, strings or collections. To find an element at a relative position from the end of an array, one needs to get the length and subtract the position to get the element, as shown below:
let arr = [1, 2, 3, 4, 5];
console.log(arr[arr.length-2]); // 4
It is not possible to apply negative indices to the array accessor, as the syntax is not specific to arrays and strings. The accessor can be used with objects to get the value of a property. For example, we can do the following with objects:
let obj = {
name: 'Ravi'
};
let name = obj["name"];
The .at() proposal is to add a new method on iterable types to allow the negative indices. To get the second last item from the array shown in the snippet above, we can do it as follows:
console.log(arr.at(-2));
Disclaimer: The method .at() is not yet available in any of the JavaScript platforms. The polyfill mentioned in the proposal of this feature can be used to play with it before it gets released.
RegExp Match Indices
This proposal introduces a new regexp flag that can be added at the end of a regular expression. It adds additional information in the result obtained after executing the expression on a string.
The new flag, d stands for index and it adds information related to indices of the full substring matched in the bigger string and the indices of the individual segments. Let’s look at an example to understand this, consider the following snippet:
let text = 'This is a sample text, it will be used in the example for the regexp indices';
let exp = /(sample+)/d;
let matches = exp.exec(text);
console.log(matches.indices);
The output of this snippet is shown in the following image:
As we see, the indices property of the matches object has a list of pairs of indices and groups. The groups property is null here as the regexp didn’t define any groups.
The first entry in the array of indices indicates the start and end indices of the full substring in which the matches were found. The subsequent entries have the indices of the individual match. As the text sample matches the expression fully, the entries have the same values.
Let’s modify the expression and see how it behaves. The following snippet modifies the expression to contain multiple matches:
let text = 'This is a sample text will be used as a for the regexp indices';
let exp = /(T+)(his+)/d;
let matches = exp.exec(text);
console.log(matches);
The following image shows the output of this snippet:
The expression has two parts. So the indices have multiple entries now.
- The first entry in the indices contains start and end indices of the complete substring, which is of the substring “This” and the indices are from 0 to 4
- The second entry is for the substring that matches the first part of the regex. It matches with the letter “T” and the indices are from 0 to 1
- The third entry is for the substring that matches with the second part of the reges. It matches with “his” and the indices are from 1 to 4
If the regex contains groups, the indices.groups property would be set to the indices of each group. Let’s consider the following snippet:
let text = 'This is a sample text will be used as an example for regexp indices';
let exp = /(?<gr1>sample+) (?<gr2>text+)/d;
let matches = text.match(exp);
console.log(matches);
The following image shows the output of this snippet:
Now the groups property in both matches and indices is set and we are able to see the values of substrings matched in the matches object. We also see indices of each group in the groups property of the indices object.
Similarly, the response of String.prototype.match and String.prototype.matchAll will have the additional indices information if the flag d is used with the regular expression passed to these methods.
Top-level await
This proposal is to support the use of the await keyword at the top level in an ES module. This makes the module using the await keyword a big async function and makes the importing modules to wait till the operation is completed. Node.js developers would have used this feature before others, as the proposal has been in review since 2014 and it was implemented in Node.js v13.8 about a year ago.
Today, we don’t have a direct way of exporting a value from a module which we get in response to an API call or from any other asynchronous source. There are multiple work-arounds to achieve this today. Following are two of the approaches I can think of now:
- A promise can be exported from the module, but the importing modules need to handle the data or errors when the promise gets resolved or rejected
- A variable can be exported from the module and its value can be set after the asynchronous operation completes. The difficulty with this approach is, the importing module needs to check if the variable is set to value or not and it won’t be clearly known to the importing module whether the asynchronous call is completed when it uses the value
Top-level await solves these issues and provides a reliable and predictable way to import an asynchronous module. Let’s see an example for using top level await. The following snippets show the contents of two of the files in a simple Node.js application:
// server-request.js
import axios from 'axios';
let result;
try {
let repoDetails = await axios.get('https://api.github.com/repos/tc39/proposals');
result = repoDetails.data.description;
}
catch (e) {
console.log('Error occurred', e);
}
export { result };
Snippet - 10
// client.js
import { result } from './server-request.js';
console.log("Result is: ", result);
As we see here, the file server-request.ts makes a request to the GitHub REST API and it uses the await keyword at the top level. The client.ts file imports the server-request module and it prints the imported value on the console.
This code has to be executed on Node.js version 13 or above. It also requires the type property in the file package.json to be set to module, as shown below:
{
"dependencies": {
"axios": "0.19.2"
},
"type": "module"
}
If you are using a Node.js version older than 14.8.0, you need to use the –harmony-top-level-await flag with the node command. With version 14.8 and above the flag is not required anymore. The following snippet shows the command with the flag:
> node --harmony-top-level-await client.js
Disclaimer: Top level await cannot be used on the browsers yet
Features Added to Classes
Fields, Private instance methods and accessors
The classes in current JavaScript don’t offer a way to make the fields or methods private. So it is difficult to hide the information that should not get accessed outside the class. This proposal adds the support for private fields and methods in the JavaScript classes. A point to remember is, constructors cannot be made private yet in JavaScript, they need to be public.
The proposal also adds the capability to declare the class fields before using them and without assigning any values to them. This feature makes the classes more documented for the developers working in the team, as one need not go looking for all the “this.” references in the class body to find all the members of the class.
A private field or method has to be prefixed with the pound (#) symbol. Any member of the class defined using the pound symbol is not accessible outside the class, it can be accessed only inside the class. The following example shows how to use the class fields and private members:
class Person {
#firstName;
#lastName;
#dob;
constructor(firstName, lastName, dob) {
this.#dob = dob;
this.#firstName = firstName;
this.#lastName = lastName;
}
get age() {
let today = new Date();
return today.getFullYear() - this.#dob.getFullYear();
}
#fullName() {
return `${this.#firstName} ${this.#lastName}`;
}
printDetails() {
console.log(`The person ${this.#fullName()} is aged ${age} years`);
}
}
let p = new Person("Ram", "Kumar", new Date(1990, 2, 4));
p.printDetails();
console.log(p.#dob); // error
The following list describes the usage of fields and private variables in the above class:
- The fields #firstName, #lastName and #dob are private fields to the class. These can be used only inside the class Person
- All the private fields are declared at the beginning of the class, before they are used and they are not set to any value here. Their presence was just declared to make the class more readable
- The class also defines a private method #fullName
- Any attempt to access the private variables outside the class will result in an error. The attempt to use the private field #dob results in an error here, it displays the following error on the browser console:
Note: While field declaration makes the code more readable, it could lead to clutter if private and public variables are jumbled together in one set of declarations. It is good to keep the declarations of private and public members separated.
Static Class Features
This proposal adds the support for static public fields, static private methods and static private fields to the class. The static members are specific to the class and not to the instances. They are created when the class gets referenced for the first time and they are created only once in the lifetime of the application.
The following snippet shows a class using the static features of the class:
class Person {
static #count = 0;
#name;
constructor(name) {
Person.#count++;
this.#name = name;
}
static isCreated() {
return this.#count > 0;
}
static getCount() {
return this.#count;
}
static #resetCount() {
this.#count = 0;
}
static getInstance() {
this.#resetCount();
let p = new Person('Ram');
p.#name = 'Krishna';
}
}
let p = Person.getInstance();
console.log(`Count is: ${Person.getCount()}`);
The following listing describes the features used in snippet:
- The class declares a private static field #count and uses it inside the static methods as well as the constructor
- The static method #resetCount is private to the class and it cannot be accessed outside
- The static property isCreated is public and it can be accessed outside using the class reference
- The static members can be accessed inside the static methods with the this keyword and inside the instance methods using the class name
- Private static members are accessible inside the instance methods and vice-versa
Any attempt to access the private static members results in an error similar to the error we get on accessing the private instance fields.
Static Blocks
The static blocks provide a way to initialize the static fields of the class. They get executed only once in the lifetime of the application and it happens when the class is referenced for the first time.
In the snippet, the static field #count is initialized during declaration. It works for this case to be initialized here, but there could be scenarios where the static field is dependent on some other factors and needs to be assigned with a computed value. A static block can be used to write the logic required to perform the computation and then assign the value.
Say, we want to add a restriction to the person object saying the date of birth of the person should be at least 30 years prior to the current year. For this we can create a static variable to hold the year and initialize it in a static block. The following snippet shows this example:
class Person {
#name;
#dob;
static #maxYear;
static {
this.#maxYear = (new Date().getFullYear()) - 30;
}
set dateOfBirth(dob) {
if(dob.getFullYear() - Person.#maxYear < 0) {
throw new Error("Invalid dob");
}
this.#dob = dob;
}
}
As the static block has access to all the private static and private instance fields, it can be used to perform even some complex logic that involves the usage of private fields. The example in the proposal of this feature shows how the static block can be used to initialize an object in the module.
Ergonomic brand checks for Private Fields
While using the instance fields (private or public) of an object inside the static methods or in the instance methods of another object of the same class, we need to be careful and check if the field exists in the object. Otherwise it may lead to some exceptions.
To check if a property exists, we generally use if conditions or ternary operators. The following snippet shows if a property exists in the object using an if condition:
class MyClass {
#name;
static foo(obj) {
if (obj.#name) {
// logic
}
}
}
The downside of this approach is, the if condition fails even when the field #name exists in the object and is assigned with the falsy values. It doesn’t actually check for the existence, it rather checks the value of the field.
The in keyword can be used to check if a property exists in an object. This approach seems more reliable as it checks for the presence and doesn’t take value into the account to confirm the existence. The following snippet shows how it is used today with objects:
let obj = {
name: 'Ravi',
age: 25
};
if("age" in obj) { // checks if obj has age
console.log(`Object has the property age and the value of the property is: ${obj.age}`);
}
The proposal for ergonomic brand checks extends this capability of checking the existence of the private fields in a class using the in keyword. The above snippet can corrected using the in keyword and re-written as shown below:
class MyClass {
#name;
static foo(obj) {
if (#name in obj) {
// logic
}
}
}
The expression inside the if condition is now replaced using the in keyword. Now this expression means “does obj have the field #name?”. It evaluates to true even when the value of the field is set to null or undefined.
Error Cause
Error handling is a crucial part of an application. The errors are not used just to the handle abnormal scenarios in the code, but they are also used to provide documentation for the users. If the error thrown from a place in the code doesn’t have enough information the user will have a harder time in figuring out the reason behind it before finding a fix.
ES 2022 adds a property to the error objects to help the authors of the errors to provide additional information describing cause of the error. The custom errors created to rethrow a generated error can include the property cause to describe the reason behind the error. The following snippet shows an example of this property:
try {
await fetch('https://api-url-path');
}
catch(error) {
throw new Error('API couldn\'t be invoked', { cause: error });
}
It is also possible to provide more structured data to the cause property. The structured data is useful when the error object needs to be processed by the code that uses the functionality containing the custom error. In my opinion, this feature is useful for the library authors, as it provides a way for them to add additional information about the error that can be programmatically read by the consuming code and the application can handle the error in a better way. Snippet below shows an example of this feature.
function saveEmployee(employee) {
if(Number.isNaN(employee.id)) {
throw new Error("Invalid employee", { cause: {
{ code: "NonInteger", values: employee.id }
} });
}
// login to save employee
}
Conclusion
The features that are added in ES 2022 offer a lot of new capabilities with classes, make the modules asynchronous with top level await and other features make the language a better fit for bigger applications. I hope this article provides a good start to these features and excites you to look forward for the next release of the language.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active
blogger, an author at
SitePoint and at
DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community