The Latest ES13 JavaScript Features

By Roman Melnik

June 20th, 2022

image

ECMAScript 2022 (ES13) is a new JavaScript standard set to be released in June 2022. Let's go through an overview of the last few changes that are to be in the new release as they finished proposals (those that have reached Stage 4 in the proposal process and thus are implemented in several implementations and will be in the next practical revision).

ECMAScript 2022 features nine finished proposals:

Class field declarations

Before ES13 we would define properties of a class in its constructor like this:

class User {
  constructor() {
    // public field
    this.name = "Tom";
    // private field
    this._lastName = "Brown";
  }

  getFullName() {
    return `${this.name} ${this._lastName}`;
  }
}

const user = new User();
user.name;
// "Tom"

user._lastName;
// "Brown"
// no error thrown, we can access it from outside the class

Inside the constructor, we defined two fields. As you can see one of them is marked with an _ in front of the name which is just a JavaScript naming convention to declare the field as private meaning that it can only be accessed from inside of a class method. But, that's just a naming convention, that's why when we tried to access it, it didn't raise any error.

In ES13 we have an easier way to declare both public and private fields. The first thing is that we don't have to define them inside of the constructor. Secondly, we can also define private fields by pre-pending # to their names.

class User {
  name = "Tom";
  #lastName = "Brown";

  getFullName() {
    return `${this.name} ${this.#lastName}`;
  }
}

const user = new User();
user.name;
// "Tom"

user.getFullName();
// "Tom Brown"

user.#lastName;
// SyntaxError - cannot be accessed or modified from outside the class

The main difference with the previous example is that this time an actual error will be thrown if we try to access or modify the field outside of the class.

Ergonomic brand checks for private fields

This functionality helps us to check that the object has the given private slot in it, for this purpose, the in operator is used.

class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

Person.check(new Person()), // true

Using the same private identifier in different classes. The two classes User and Person both have a slot whose identifier is #name. The in operator distinguishes them correctly:

class User {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

User.check(new User()),     // true
User.check(new Person()),   // false
Person.check(new Person()), // true
Person.check(new User()),   // false

Class Static Block

Static initialization blocks in classes. For static data, we have Static fields and Static Blocks that are executed when the class is created. A class can have any number of static {} initialization blocks in its class body. These are evaluated, along with any interleaved static field initializers, in the order, they are declared. The super.propertycan be used inside a static block to reference properties of a super class.

class Dictionary {
  static words = ["yes", "no", "maybe"];
}

class Words extends Dictionary {
  static englishWords = [];
  static #localWord = "ok";
  // first static block
  static {
    // link by super to the  Dictionary class
    const words = super.words;
    this.englishWords.push(...words);
  }
  // second static block
  static {
    this.englishWords.push(this.#localWord);
  }
}

console.log(Words.englishWords);
//Output -> ["yes", "no", "maybe", "ok"]

This example below shows how access can be granted to the private object of a class from an object outside the class

let getClassPrivateField;

class Person {
  #privateField;
  constructor(value) {
    this.#privateField = value;
  }
  static {
    getClassPrivateField = (obj) => obj.#privateField;
  }
}

getClassPrivateField(new Person("private value"));

Regexp Match Indices

This upgrade will allow us to use the d character to specify that we want to get the indices (starting and ending) of the matches of our RegExp. Previously this was not possible. You could only obtain indexing data in the process of string-matching operation.

const fruits = "Fruits: apple, banana, orange";
const regex = /(banana)/g;

const matchObj = regex.exec(fruits);

console.log(matchObj);
// [
//   'banana',
//   'banana',
//   index: 15,
//   input: 'Fruits: apple, banana, orange',
//   groups: undefined
// ]

What we don't know are the indices at which the string ends, something that we can add now the d character and see the result.

const fruits = "Fruits: apple, banana, orange";
const regex = /(banana)/dg;

const matchObj = regex.exec(fruits);

console.log(matchObj);
// [
//   'banana',
//   'banana',
//   index: 15,
//   indices:[
//      [15, 21],
//      [15, 21]
//   ]
//   input: 'Fruits: apple, banana, orange',
//   groups: undefined
// ]

New field indices as you can see it returned [15,21]

We can use Regexp.exec or String.matchAll to find a list of matches, with the main difference between them being that Regexp.exec returns its results one by one whereas String.matchAll returns an iterator.

Await operator at the top-level

The await operator can only be used within an async method is probably an error you have encountered frequently. In ES13 we will be able to use it outside of the context of an async method.

Loading modules dynamically

const strings = await import(`./example.mjs`);

Using a fallback if module loading fails

let jQuery;
try {
  jQuery = await import("https://cdn-a.com/jQuery");
} catch {
  jQuery = await import("https://cdn-b.com/jQuery");
}

Using whichever resource loads fastest

const resource = await Promise.any([
  fetch("http://example1.com"),
  fetch("http://example2.com"),
]);

Method .at() function for Indexing

Currently, to access a value from the end of an indexable object, the common practice is to write arr[arr.length - N], where N is the Nth item from the end (starting at 1). This requires naming the indexable twice and additionally adds 7 more characters for the .length.

Another method that avoids some of those drawbacks, but has some performance drawbacks of its arr.slice(-N)[0]

const arr = [100, 200, 300, 400];
arr[0]; // 100
arr[arr.length - 2]; // 300
arr.slice(-2)[0]; // 300

We would be able to write:

const arr = [100, 200, 300, 400];
arr.at(0); // 100
arr.at(-2); // 300

const str = "ABCD";
str.at(-1); // 'D'
str.at(0); // 'A'

The following "indexable" types have method .at():

  • string
  • Array
  • All Typed Array classes: Uint8Array etc.

Accessible Object.prototype.hasOwnProperty()

In JavaScript, we already have an Object.prototype.hasOwnProperty but, as the MDN documentation also suggests, it's best to not use hasOwnProperty outside the prototype itself as it is not a protected property, meaning that an object could have its property called hasOwnProperty that has nothing to do with Object.prototype.hasOwnProperty

const person = {
  name: "Roman",
  hasOwnProperty: () => {
    return false;
  },
};

person.hasOwnProperty("name"); // false

Another problem Object.create(null) will create an object that does not inherit from Object.prototype, making those methods inaccessible.

Object.create(null).hasOwnProperty("name");
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function

The Object.hasOwn() method with the same behavior as calling Object.hasOwnProperty, takes our Object as the first argument and the property we want to check as the second:

const object = { name: "Mark" };
Object.hasOwn(object, "name"); // true

const object2 = Object.create({ name: "Roman" });
Object.hasOwn(object2, "name"); // false
Object.hasOwn(object2.__proto__, "name"); // true

const object3 = Object.create(null);
Object.hasOwn(object3, "name"); // false

Error Cause

To help unexpected behavior diagnosis, errors need to be augmented with contextual information like error messages, and error instance properties to explain what happened at the time, .cause property on the error object would allow us to specify which error caused the other error. So errors can be chained without unnecessary and over-elaborate formalities on wrapping the errors in conditions.

try {
  apiCallThatCanThrow();
} catch (err) {
  throw new Error("New error message", { cause: err });
}

Array find from last

In JavaScript, we already have an Array.prototype.find and Array.prototype.findIndex. We know to find from last may have better performance (The target element on the tail of the array, could append with push or concat in a queue or stack, eg: recently matched time point in a timeline). If we care about the order of the elements (May have a duplicate item in the array, eg: last odd in the list of numbers), a better way to use new methods Array.prototype.findLast and Array.prototype.findLastIndex

Instead of writing for find from last:

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

// find
[...array].reverse().find((n) => n.value % 2 === 1); // { value: 3 }

// findIndex
array.length - 1 - [...array].reverse().findIndex((n) => n.value % 2 === 1); // 2
array.length - 1 - [...array].reverse().findIndex((n) => n.value === 9); // should be -1, but 4

We would be able to write:

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

// find
array.findLast((n) => n.value % 2 === 1); // { value: 3 }

// findIndex
array.findLastIndex((n) => n.value % 2 === 1); // 2
array.findLastIndex((n) => n.value === 9); // -1

Final notes

So we have learned about the last ES13 JavaScript features that would improve your efficiency. These ES13 JavaScript features are all set to be launched in June of 2022. Let's wait for the release.



Continue Learning