JavaScript-Series-#132-New-Features-in-ES2021
ECMAScript, the standardization behind JavaScript, consistently evolves, bringing new features and improvements with each annual release. ES2021 (also known as ECMAScript 2021 or ES12) continued this tradition, introducing a set of enhancements designed to make JavaScript development more efficient, readable, and robust. This installment of our JavaScript series dives into some of the most impactful additions from ES2021, providing clear explanations and practical code examples.
1. String.prototype.replaceAll()
Prior to ES2021, replacing all occurrences of a substring in a string often required using a regular expression with the global flag (/g) or iterating through the string. While effective, it wasn't the most straightforward or intuitive for simple string-to-string replacements. The new replaceAll() method addresses this directly.
The replaceAll() method allows you to replace all instances of a specified substring with another string, without needing regular expressions. It behaves exactly as you'd expect, making string manipulation cleaner.
Example:
const originalString = "I love JavaScript. JavaScript is awesome!";
// Old way using regex (or manual iteration)
const replacedRegex = originalString.replace(/JavaScript/g, "TypeScript");
console.log(replacedRegex);
// Output: "I love TypeScript. TypeScript is awesome!"
// New way with replaceAll()
const replacedAll = originalString.replaceAll("JavaScript", "TypeScript");
console.log(replacedAll);
// Output: "I love TypeScript. TypeScript is awesome!"
const anotherString = "The quick brown fox jumps over the lazy dog.";
const modifiedString = anotherString.replaceAll("o", "_");
console.log(modifiedString);
// Output: "The quick br_wn f_x jumps _ver the lazy d_g."
2. Promise.any() and AggregateError
Promise.any() is a powerful addition to the Promise API that complements existing methods like Promise.all(), Promise.race(), and Promise.allSettled(). While Promise.race() resolves or rejects as soon as any promise settles (regardless of success or failure), Promise.any() resolves as soon as any of the input promises fulfills. It only rejects if all of the input promises reject, at which point it rejects with an AggregateError.
An AggregateError is a new error type introduced alongside Promise.any(). It groups multiple errors into a single error object, allowing you to inspect all the individual rejection reasons when Promise.any() fails.
Example:
const promise1 = new Promise((resolve, reject) => setTimeout(() => reject("Promise 1 failed"), 100));
const promise2 = new Promise((resolve, reject) => setTimeout(() => resolve("Promise 2 fulfilled!"), 200));
const promise3 = new Promise((resolve, reject) => setTimeout(() => reject("Promise 3 failed"), 150));
Promise.any([promise1, promise2, promise3])
.then(value => {
console.log("Promise.any() resolved with:", value);
// Output: "Promise.any() resolved with: Promise 2 fulfilled!" (since promise2 fulfills first)
})
.catch(error => {
console.error("Promise.any() rejected with:", error);
});
// Example of Promise.any() rejecting with AggregateError:
const failingPromise1 = new Promise((resolve, reject) => setTimeout(() => reject("Error A"), 300));
const failingPromise2 = new Promise((resolve, reject) => setTimeout(() => reject("Error B"), 200));
Promise.any([failingPromise1, failingPromise2])
.then(value => console.log("Resolved:", value))
.catch(error => {
console.error("All promises failed. AggregateError:", error);
console.error("Individual errors:", error.errors);
// Output: Individual errors: ["Error A", "Error B"]
});
3. Logical Assignment Operators (`&&=`, `||=`, `??=`)
These new operators provide a concise syntax for assigning a value to a variable only if a certain logical condition is met. They combine logical operations (AND, OR, Nullish Coalescing) with assignment, reducing boilerplate code and improving readability.
x &&= y(Logical AND assignment): Assignsytoxonly ifxis truthy. Equivalent tox = x && y.x ||= y(Logical OR assignment): Assignsytoxonly ifxis falsy. Equivalent tox = x || y.x ??= y(Nullish Coalescing assignment): Assignsytoxonly ifxisnullorundefined. Equivalent tox = x ?? y.
Example:
// Logical AND assignment (&&=)
let a = 10;
a &&= 5; // a is truthy (10), so a becomes 5
console.log(a); // Output: 5
let b = 0;
b &&= 5; // b is falsy (0), so b remains 0
console.log(b); // Output: 0
// Logical OR assignment (||=)
let c = null;
c ||= "default"; // c is falsy (null), so c becomes "default"
console.log(c); // Output: "default"
let d = "existing";
d ||= "default"; // d is truthy, so d remains "existing"
console.log(d); // Output: "existing"
// Nullish Coalescing assignment (??=)
let e = null;
e ??= "fallback"; // e is null, so e becomes "fallback"
console.log(e); // Output: "fallback"
let f = undefined;
f ??= "fallback"; // f is undefined, so f becomes "fallback"
console.log(f); // Output: "fallback"
let g = 0;
g ??= "fallback"; // g is 0 (not null/undefined), so g remains 0
console.log(g); // Output: 0
4. Numeric Separators (`_`)
Reading and understanding large numbers can sometimes be challenging, especially when dealing with many digits. ES2021 introduced numeric separators (the underscore character _) to improve the readability of numeric literals. The underscore can be used anywhere within a number to visually separate groups of digits, without affecting the numerical value itself.
Example:
const million = 1_000_000;
const billion = 1_000_000_000;
const hexValue = 0xAF_B3_D2;
const binaryValue = 0b1010_0011_1101;
const largeFloat = 1_234.567_89;
console.log(million); // Output: 1000000
console.log(billion); // Output: 1000000000
console.log(hexValue); // Output: 11529170
console.log(binaryValue); // Output: 2621
console.log(largeFloat); // Output: 1234.56789
// The value remains the same, only the visual representation changes.
console.log(1_000_000 === 1000000); // Output: true
5. WeakRef and FinalizationRegistry
These two features are more advanced and delve into memory management, primarily for scenarios where you need to interact directly with the JavaScript garbage collector. They are not intended for everyday use but are crucial for specific performance-critical or low-level library implementations.
WeakRef(Weak Reference): Allows you to hold a "weak" reference to an object. Unlike a normal (strong) reference, a weak reference does not prevent the garbage collector from reclaiming the object if there are no other strong references to it. If the object is garbage-collected, the weak reference becomes "dead" and can no longer access the object. This is useful for building caches or maps where you don't want the cached items to prevent their underlying objects from being collected.FinalizationRegistry: Provides a way to register a callback function that will be invoked when an object is garbage-collected. This allows for cleanup tasks (e.g., closing file handles, releasing external resources) to be performed after an object is no longer reachable.
It's important to use WeakRef and FinalizationRegistry with caution, as improper use can introduce complex memory management issues and unpredictable behavior. They are powerful tools but come with a steep learning curve.
Conceptual Example (simplified):
let obj = {};
const weakRef = new WeakRef(obj);
// At this point, weakRef "points" to obj.
// If 'obj' is still strongly referenced elsewhere, it exists.
obj = null; // Remove the strong reference to the object.
// Now, if the garbage collector runs, the original object might be reclaimed.
// To access the object through the weak reference:
const dereferencedObj = weakRef.deref();
if (dereferencedObj) {
// Object still exists
console.log("Object is still alive:", dereferencedObj);
} else {
// Object has been garbage-collected
console.log("Object has been garbage-collected.");
}
// FinalizationRegistry example (conceptual usage)
const registry = new FinalizationRegistry(value => {
console.log(`Object with value "${value}" has been garbage-collected. Performing cleanup.`);
// Perform cleanup for the object associated with 'value'
});
let resource = { id: 123, data: "some data" };
registry.register(resource, resource.id); // Register 'resource' for finalization, passing its 'id' as the value
resource = null; // Make the object eligible for garbage collection
// When 'resource' is garbage collected, the registry's callback will eventually fire.
Conclusion
ES2021 delivered a collection of features that, while not revolutionary individually, collectively enhance the developer experience and expand JavaScript's capabilities. From simplifying string operations with replaceAll() and improving Promise handling with Promise.any() to refining syntax with logical assignment operators and numeric separators, these updates underscore JavaScript's continuous evolution. Understanding and leveraging these new features can lead to more concise, readable, and maintainable code in your projects.
As always, keeping up with the latest ECMAScript releases is key to staying proficient and productive in the ever-changing landscape of web development.