This article was originally posted on my blog. Head over to inspiredwebdev.com for more articles and tutorials. Check out my JavaScript course on Educative to learn everything from the basics to ES2022.
Every year since 2015, JavaScript
has been receiving constant yearly updates to its specification with new interesting features added.
In this article we will have a look at what's to come since many features already reached stage 4 and will be included in the specification.
For those of you who don't know, there are 4 stages in the proposal process, with the 4th one being the last one which marks the proposal as finished.
As a developer, it's important to stay updated with the new specs of a language and if you feel like you've been left behind by the many updates that JavaScript
received in the past years, I can recommend you my book that covers everything from the basics of the language all the way to the latest ES2022 specs, including a little intro to TypeScript
. You can read it for free on Github where you will also find links where to buy the ebook or you can check out my course on Educative
Now, let's get started with the first of the new ES2022 features:
Class Fields
Class public Instance Fields & private Instance Fields
Before ES2022 we would define properties of a class
in its constructor
like this:
class ButtonToggle extends HTMLElement {
constructor(){
super();
// public field
this.color = 'green'
// private field
this._value = true;
}
toggle(){
this.value = !this.value
}
}
const button = new ButtonToggle();
console.log(button.color);
// green - public fields are accessible from outside classes
button._value = false;
console.log(button._value);
// false - no error thrown, we can access it from outside the class
Inside of 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. Of course, that's just a naming convention and not something that the language itself enforces and that's why when we tried to access it, it didn't raise any error.
In ES2022 we have an easier way to declare both public
and private
fields. Let's have a look at this updated example:
class ButtonToggle extends HTMLElement {
color = 'green';
#value = true;
toggle(){
this.#value = !this.#value;
}
}
const button = new ButtonToggle();
console.log(button.color);
// green - public fields are accessible from outside classes
// SyntaxError - cannot be accessed or modified from outside the class
console.log(button.#value);
button.#value = false;
The first thing to notice is that don't have to define them inside of the constructor
. Secondly, we can also define private
fields by pre-pending #
to their names.
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.
Private methods and getter/setters for JavaScript classes
Similar to how we did in the previous example, we can also define private
methods and getter/setters for our classes.
class ButtonToggle extends HTMLElement {
color = 'green'
#value = true;
#toggle(){
this.#value = !this.#value
}
set #setFalseValue(){
this.#value = false;
}
}
const button = new ButtonToggle();
// SyntaxError - cannot be accessed or modified from outside the class
button.#toggle();
// SyntaxError - cannot be accessed or modified from outside the class
button.#setFalseValue;
In the example above we replaced toggle()
with #toggle()
thus making the toggle
method private
and only accessible from inside of the class
.
Static class fields and private static methods
A static
field or method is only accessible in the prototype and not in every instance of a class
and ES2022 provides us with the means to define static
fields and static
public/private methods by using the static
keyword.
Previously we would have to define them outside of the class
body such as:
class ButtonToggle extends HTMLElement {
// ... class body
}
ButtonToggle.toggle(){
// static method define outside of the class body
}
Now, instead, we can define them directly inside of the class
body with the use of the static
keyword:
class ButtonToggle extends HTMLElement {
#value = true;
static toggle(){
this.#value = !this.#value
}
}
// this will work
ButtonToggle.toggle();
// SyntaxError - private static field
const button = new ButtonToggle();
button.toggle();
As you can see in the example above, we can access toggle()
directly on our ButtonToggle
but we cannot do the same on a new instance of it.
We can use the static
keyword in front of fields and methods (both private and public) and by combining it with the #
(private
) we can create a private static
method only accessible from inside of our prototype class
.
class ButtonToggle extends HTMLElement {
#value = true;
static #toggle(){
this.#value = !this.#value
}
}
// this will error, it's a private static method
ButtonToggle.#toggle();
Ergonomic brand checks for private Fields
As we saw in the examples above, if we try to access a private
field outside of a class
it will throw an exception and will not return undefined
like it does with public
fields.
We could try using a simple try/catch
inside of the class
to check if the field exists:
class ButtonToggle extends HTMLElement {
// initialised as null
#value = null;
get #getValue(){
if(!this.#value){
throw new Error('no value');
}
return this.#value
}
static isButtonToggle(obj){
try {
obj.#getValue;
return true;
} catch {
// could be an error internal to the getter
return false;
}
}
}
In the example above we added a private
getter
that will throw an error if there is no value yet. We then created a static
method to access that getter
and tried to determine if it exists by checking with a try/catch
. The problem lies in the fact that we don't know if the code in the catch
is executed because the getter
is not present or simply because it threw an error.
ES2022 provides us with an easy way to check if said field belongs to a class
by using the operator in
. Let's rework our example code:
class ButtonToggle extends HTMLElement {
// initialised as null
value = null;
get #getValue(){
if(!this.#value){
throw new Error('no value');
}
return this.#value;
}
static isButtonToggle(obj){
return #value in obj && #getValue in obj
}
}
Our method isButtonToggle
will check if the class
contains the private
fields 'value' and 'getValue'.
Class Static Block
This is yet another upgrade to the static
fields in ES2022 that allows us to have static
blocks inside of classes. The issue this is trying to solve arises from the fact that we cannot evaluate statements such as a try/catch
during initialization meaning that we would have to put that code outside of the class
body:
class ButtonToggle{
value = false;
get getValue(){
if(!this.#value){
throw new Error('no value');
}
return this.#value
}
}
// this has to sit outside of the class body
try {
const val = ButtonToggle.getValue;
ButtonToggle.value = val
} catch {
ButtonToggle.value = false
}
As you can see, our try/catch
had to be put outside of the class
body. Thankfully we can replace that with a static
block like the following:
// method defined outside of the class body
let initVal;
class ButtonToggle{
#value = false;
get getValue(){
if(!this.#value){
throw new Error('no value');
}
return this.#value
}
static {
initVal = () => {
this.#value = this.getValue;
}
}
}
initVal();
We created a static
block inside of our class
that defines a function that we declared outside of the context of that class
. As you can see, the method will have access to '#value' which is a private
field or our class. They will have access to private
methods and fields, being them instance-private
(meaning non static
, private
fields) or static-private
.
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.
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. Let's see them in practice:
const fruits = 'Fruits: mango, mangosteen, orange'
const regex = /(mango)/g;
// .exec
RegExp(regex).exec(fruits);
// [
// 'mango',
// index: 8,
// input: 'Fruits: mango, mangosteen, orange',
// groups: undefined
// ]
// matchAll
const matches = [...fruits.matchAll(regex)];
matches[0];
// [
// 'mango',
// 'mango',
// index: 8,
// input: 'Fruits: mango, mangosteen, orange',
// groups: undefined
// ]
Both return the index of the match, the match itself, and the initial input. What we don't know are the indices at which the string ends, something that we will now be able to do like this:
const fruits = 'Fruits: mango, mangosteen, orange'
// /gd instead of the previous /g
const regex = /(mango)/gd;
const matches = [...fruits.matchAll(regex)];
matches[0];
// [
// "mango",
// "mango",
// groups: undefined
// index: 8
// indices:[]
// [8, 13],
// [8, 13]
// ]
// groups: undefined
As you can see it returned [8,13] as the indices of the first occurrence of 'mango' in our string.]
Top-level await
"await
operator can only be used within an async
method" is probably an error you have encountered frequently. In ES2022 we will be able to use it outside of the context of an async
method in our modules. For example, we could defer the execution of a module and its parent until something else is imported.
This can be useful in many scenarios, for example when we have a dynamic path for a dependency that depends on a runtime value:
// we need to get the appropriate translation keys based on the language
const translationKeys = await import(`/i18n/${navigator.language}`);
Another use could be to provide a fallback for a dependency:
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
.at()
In JavaScript
you can do arr[1]
to access the value at index 1 of an Array
but you cannot do arr[-1]
to count backward from the ending of the Array
. The reason is that the brackets syntax is used not only for arrays but also for Objects, where obj[-1]
would simply refer to the property '-1' of that Object
.
With the .at()
method we now have an easy way to access any index, positive or negative of arrays and strings:
const arr = [10,20,30,40];
// same -> 10
arr[1];
arr.at(1);
// same -> 40
arr[arr.length -1];
arr.at(-1);
Note that a negative value simply means: 'Start counting backward from the end of the array'.
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
.
For example:
const obj = {
hasOwnProperty:()=> {
return false
}
}
obj.hasOwnProperty('prop'); // false
As you can see, we defined our own method hasOwnProperty
that has overridden the one on the prototype, an issue that is not present with Object.hasOwn()
.
Object.hasOwn()
takes our Object
as the first argument and the property we want to check as the second:
const student = {
name: 'Mark',
age: 18
}
Object.hasOwn(student,'age'); // true
Object.hasOwn(student,'grade'); // false
What's the feature you are most excited to try? Leave a comment down below.
If you want to learn everything about JavaScript from the basics all the way to ES2022, please check out my book available to read for free on Github. A course is also on Educative
Top comments (3)
I'm already in love with .at()
Great Article. The Top-level await feature is the one I am looking forward to the most.
hey, fix the below code, if it is wrong: