See the ES2021 standard for full specification of the ECMAScript 2021 language.
ES2021 includes the following new feature proposals:
- Logical Assignment Operators
String.replaceAll()
Promise.any()
andAggregateError
- Numeric Separator
Intl.ListFormat
dateStyle
andtimeStyle
options forIntl.DateTimeFormat
WeakRef
s andFinalizationRegistry
objects
The new logical assignment operators—&&=
, ||=
, and ??=
—combine logical operations with the assignment operation. These new operators are similar to the existing logical operators and will allow you to assign default values to variables.
Similar to the logical OR operator, the logical OR assignment operator is a short-circuit operation. The expression
x ||= y
is identical to the expressionx || (x = y)
. This means thaty
will be assigned tox
, if and only if,x
constitutes a falsy value. Otherwise, it will preserve its initial value.
const setDefaultPassword = () => {
// Perform some logic to set up a default password
return 'defaultPassword'
}
const jane = {
name: 'Jane Doe',
password: 'Abc123'
}
const john = {
name: 'John Doe',
password: ''
}
jane.password ||= setDefaultPassword()
john.password ||= setDefaultPassword()
console.log(jane.password)
// Prints 'Abc123'
console.log(john.password)
// Prints 'defaultPassword'
The logical AND assignment operator is the antithesis of the logical OR assignment operator. In the expression
x &&= y
,y
is assigned tox
, if and only if,x
is a truthy value. Otherwise, it preserves its initial value.
const updateOffice = () => {
return 2
}
const jane = {
id: 1,
name: 'Jane Doe',
office: 4
}
const john = {
id: 2,
name: 'John Doe',
office: undefined
}
jane.office &&= updateOffice()
john.office &&= updateOffice()
console.log(jane.office)
// Prints 2
console.log(john.office)
// Prints undefined
The logical nullish assignment operator operates in such a way that in the expression x ??= y
, it only assigns y
to x
, if x
is nullish (i.e., if x
is either null
or undefined
).
const createEmail = () => {
// Perform some logic to create an email
return '[email protected]'
}
const jane = {
id: 1,
name: 'Jane Doe',
email: '[email protected]'
}
const john = {
id: 2,
name: 'John Doe'
}
jane.email ??= createEmail()
john.email ??= createEmail() // john.email is undefined
console.log(jane.email)
// Prints '[email protected]'
console.log(john.email)
// Prints '[email protected]'
String.replaceAll()
is a suitable method that addresses a particular shortage in String.replace()
—the inability to replace all the occurrences of a pattern with a new string (to do that, one had to use regular expressions).
Now, with the introduction of String.replaceAll()
to the language, we can easily replace all the occurrences of a given substring at once.
The String.replaceAll()
method returns a new string in which all occurrences of a pattern are replaced by a replacement passed to it. The first parameter, a pattern, can either be a string or a regex pattern, and the second parameter, a replacement, can either be a string or a function that creates a new string to replace the pattern. This method operates without mutating the original string.
const sentence =
'Interestellar is a great movie. Everybody should watch the movie at least once.'
sentence.replace('movie', 'film')
// Returns 'Interestellar is a great film. Everybody should watch the movie at least once.'
// Only the first occurrence of 'movie' is replaced.
sentence.replaceAll('movie', 'film')
// Returns 'Interestellar is a great film. Everybody should watch the film at least once.'
// All occurrences of 'movie' are replaced.
sentence.replaceAll('movie', () => 'flick')
// Returns 'Interestellar is a great flick. Everybody should watch the flick at least once.'
// All occurrences of 'movie' are replaced with the string returned by the callback function.
Promise.any([promise1, promise2, promise3, ...]).then(...do something)
Promise.any()
is a new promise method that takes in an iterable of Promise
objects and, as soon as one of the promises in the iterable fulfills, it returns a single promise that resolves to the value from that first fulfilled promise. If no promises in the iterable fulfill (i.e., if all of the given promises are rejected), then the returned promise is rejected with an AggregateError
—a new subclass of Error
that groups together individual errors. In essence, the Promise.any()
method is the opposite of Promise.all()
.
const promise1 = Promise.reject(0)
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'))
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'))
const promises = [promise1, promise2, promise3]
Promise.any(promises).then((value) => console.log(value))
// Prints 'quick'
Now, the scenario in which none of the promises passed to Promise.any()
fulfills (which also takes into account the case where the passed iterator is empty), we need to catch the exception and handle it.
const promise = new Promise((_, reject) => reject(new Error('Ooops!')))
try {
async function test() {
const result = await Promise.any([promise])
console.log(result)
}
test()
} catch (error) {
console.error(error.errors)
// Logs the object Promise {<rejected>: AggregateError: All promises were rejected}
// Throws the error caught (in promise) AggregateError: All promises were rejected
}
For the sake of simplicity, we passed a to-be-rejected promise to Promise.any()
. As a result, the above code logs the following error in the console.
ES2021 introduced the numeric separator to improve the readability of numeric literals by creating a visual separation between groups of digits. As a result, we can use the underscore (_
) character to separate groups of digits, just like we use commas to separate numbers in writing.
This new feature of the language helps us circumvent the difficulty the human eye faces to quickly parse large numeric literals (especially when they involve long repetition of digits), so now it could be easier for developers to get the correct value or order of magnitude of long and/or confusing numeric literals in their code.
Let's take a look at some examples:
let x = 1000000000 // Is this a billion? a hundred millions? Ten millions?
let y = 101475938.38 // What scale is this? What power of 10?
let z = 0.000001 // Is this a millionth? a billionth?
// Numeric separators to the rescue!
x = 1_000_000_000 // This is a billion
y = 101_475_938.38 // This is a hundred million plus change
z = 0.000_001 // This is a millionth
Before exploring how this new constructor works, let's recall what the Intl
object is: a namespace for the ECMAScript Internationalization API, which supplies a collection of helper methods to support internalization efforts, like language-sensitive string comparison, number formatting as well as date and time formatting.
Consequently, the Intl.ListFormat
object enables language-sensitive list formatting. Its constructor,ListFormat
, creates and returns a formatter object that—depending on the configuration passed upon creation—will join lists of strings using the best localized conventions.
Let’s take a look at an example to better illustrate this:
const javaScriptFrameworks = ['Angular', 'React', 'Vue']
const shortUnitFormatter = new Intl.ListFormat('en', {
style: 'short',
type: 'unit'
})
const narrowUnitFormatter = new Intl.ListFormat('en', {
style: 'narrow',
type: 'unit'
})
const longConjunctionFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction'
})
const longDisjunctionFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'disjunction'
})
const longConjunctionItalianFormatter = new Intl.ListFormat('it', {
style: 'long',
type: 'conjunction'
})
const longDisjunctionGermanFormatter = new Intl.ListFormat('de', {
style: 'long',
type: 'disjunction'
})
shortUnitFormatter.format(javaScriptFrameworks)
// Prints 'Angular, React, Vue'
narrowUnitFormatter.format(javaScriptFrameworks)
// Prints 'Angular React Vue'
longConjunctionFormatter.format(javaScriptFrameworks)
// Prints 'Angular, React, and Vue'
longDisjunctionFormatter.format(javaScriptFrameworks)
// Prints 'Angular, React, or Vue'
longConjunctionItalianFormatter.format(javaScriptFrameworks)
// Prints 'Angular, React e Vue'
longDisjunctionGermanFormatter.format(javaScriptFrameworks)
// Prints 'Angular, React oder Vue'
The first (optional) argument for the ListFormat
constructor is the language to be used—'en'
for English in the example above. You can also pass an array of these BCP 47 Language Tags.
The second (also optional) parameter is an object with three (also optional) fields:
-
localeMatcher
— a string with the value'lookup'
or'best fit'
that specifies which locale matching algorithm to use. The default is'best fit'
. -
style
— a string that sets the separator for the final string. It can take a value of'long'
,'short'
, or'narrow'
. -
type
— a string that sets the format of the returned string. It can take a value of'conjunction'
,'disjunction'
, or'unit'
.
Intl.DateTimeFormat
is a constructor for a language-sensitive date and time formatter, long-supported in the JavaScript ecosystem.
The new options—dateStyle
and timeStyle
—allow us to control the length of the local-specific formatting of date and time strings.
Let’s see some examples on how to use it when working with time only…
const date = Date.now()
const shortTimeFormatter = new Intl.DateTimeFormat(
'en',
{ timeStyle: 'short' }
)
const mediumTimeFormatter = new Intl.DateTimeFormat(
'en',
{ timeStyle: 'medium' }
)
const longTimeFormatter = new Intl.DateTimeFormat(
'en',
{ timeStyle: 'long' }
)
shortTimeFormatter.format(date)
// Prints '4:46 PM'
mediumTimeFormatter.format(date)
// Prints '4:46:23 PM'
longTimeFormatter.format(date)
// Prints '4:46:23 PM GMT-5'
…and with dates only:
const date = Date.now()
const shortDateFormatter = new Intl.DateTimeFormat(
'en',
{ dateStyle: 'short' }
)
const mediumDateFormatter = new Intl.DateTimeFormat(
'en',
{ dateStyle: 'medium' }
)
const longDateFormatter = new Intl.DateTimeFormat(
'en',
{ dateStyle: 'long' }
)
shortDateFormatter.format(date)
// Prints '4/22/20'
mediumDateFormatter.format(date)
// Prints 'Apr 22, 2020'
longDateFormatter.format(date)
// Prints 'April 22, 2020'
We can also combine the two options to get a date-time string:
const date = Date.now()
const dateTimeFormatter = new Intl.DateTimeFormat(
'en',
{ dateStyle: 'short', timeStyle: 'long' }
)
const spanishDateTimeFormatter = new Intl.DateTimeFormat(
'es',
{ dateStyle: 'short', timeStyle: 'long' }
)
const germanDateTimeFormatter = new Intl.DateTimeFormat(
'de',
{ dateStyle: 'short', timeStyle: 'long' }
)
dateTimeFormatter.format(date)
// Prints 'May 9, 2023 at 11:43 AM'
spanishDateTimeFormatter.format(date)
// Prints '9 de mayo de 2023, 11:43'
germanDateTimeFormatter.format(date)
// Prints '9. Mai 2023 um 11:43'
According to the WeakRefs TC39 Proposal, this proposal encompasses two major new pieces of functionality:
- Creating weak references to objects with the
WeakRef
class. - Running user-defined finalizers after objects are garbage-collected, with the
FinalizationRegistry
class.
These interfaces can be used independently or together, depending on the use case.
These new features are advanced features; their correct use takes careful thought, and they are best avoided if possible. To understand how WeakRef
works, you first need to understand the concepts of object referencing and garbage collection in JavaScript.
WeakRef
is an advanced API that provides actual weak references, enabling a window into the lifetime of an object. A weak reference to an object is not enough to keep the object alive, tough. When the only remaining references to a referent (i.e. an object which is referred to by a weak reference) are weak references, garbage collection is free to destroy the referent and reuse its memory for something else. However, until the object is actually destroyed, the weak reference may return the object even if there are no strong references to it.
A primary use for weak references is to implement caches or mappings holding large objects, where it’s desired that a large object is not kept alive solely because it appears in a cache or mapping.
A WeakRef
is created with the new WeakRef
constructor, and the value of the WeakRef
variable can be accessed via the deRef
method.
Let’s see a simple example of how to use WeakRef
:
const weakRef = new WeakRef({ name: 'Jane' })
console.log(weakRef.deref().name)
// Prints 'Jane'
In this example, WeakRef
creates a weak reference to the object passed to it. This means that whenever garbage collection needs to be performed, the JavaScript engine can safely remove the object from memory and free up space since the only reference to that object is from a WeakRef
variable. This could be ideal for WebSocket data because of their short lifespans.
Finalization is the execution of code to clean up after an object that has become unreachable to program execution. User-defined finalizers enable several new use cases, and can help prevent memory leaks when managing resources that the garbage collector doesn't know about.
WeakRef
and finalizers are two features that go together. The FinalizationRegistry
method allows you to request a callback after an object has become unreachable (garbage-collected).
First, define a registry with the callback you want to run. Then, call the .register
method on the defined registry with the object you want to observe. It will let you know exactly when the memory of the object is freed by the garbage collector.
const registry = new FinalizationRegistry(value => {
console.log(`'${value}' has been garbage-collected`);
});
registry.register({ object: 'Perform a task' }, 'testObject');
Here, the object passed to the register()
method is weakly referenced, so when the value is garbage-collected, the second parameter (testObject
) is passed to the finalizer.