Exciting JavaScript updates since 2020
JavaScript has been evolving faster than ever, with new features being added on a yearly release schedule instead of the larger combined updates like ES5 and ES6. Since 2020, many powerful and exciting features have been introduced that can significantly improve how we write and optimize our code.
Dynamic imports allow you to load modules on demand, optimizing your app’s performance. This is especially useful for code-splitting and lazy-loading features.
const bookModule = await import(`.features/books/books.js`)const books = bookModule.getBooks()console.log(books) // Output: Array of booksmatchAll() returns all matching results of a regex in a string, allowing you to iterate through matches with more control than match.
const regExp = /page (\d+)/gconst text = "Read page 1, skip to page 5"let matches = [...text.matchAll(regExp)]
for (const match of matches) { console.log(`Found page ${match[1]}`)}// Found page 1// Found page 5Promise.allSettled() allows you to handle multiple promises, even if some fail. It waits for all promises to settle, providing the result or error of each.
const promises = [fetch("/users"), fetch("/roles")]const allResults = await Promise.allSettled(promises)const errors = allResults .filter((p) => p.status === "rejected") .map((p) => p.reason)The ?? operator provides a way to assign a default value, only if the left operand is null or undefined, preventing unintended behavior with falsy values like 0 or "".
// Only falls back to 42 if settings.size is null/undefined.const size = settings.size ?? 42
// Falls back to 42 if size is null/undefined, or a falsy value like 0 or ""const size = settings.size || 42Optional chaining ?. helps safely access deeply nested object properties, avoiding errors when properties don’t exist.
const user = { profile: { name: "Alice", address: { city: "Wonderland" } } }const city = user.profile?.address?.city ?? "Unknown"console.log(city) // Output: Wonderland
const zipCode = user.profile?.address?.zip ?? "No Zip Code"console.log(zipCode) // Output: No Zip CodeThe replaceAll() method allows you to replace all instances of a substring in a string without using a regular expression.
const string = "Javascript is the best web scripting language. Javascript can be used for both front end and backend"const replaced = string.replaceAll("Javascript", "Typescript")
console.log(replaced) // Typescript is awesome. Typescript rules!Private methods are denoted with a # and are only accessible within the class, providing true encapsulation.
class Person { showName() { console.log("My name is Foo") } #showAge() { console.log("Foo is 20") }}
const foo = new Person()
foo.showName() // My name is Foofoo.showAge() // Uncaught TypeError: foo.showAge is not a functionfoo.#showAge() // Uncaught SyntaxError: reference to undeclared private field or method #showAgePromise.any() resolves as soon as one of the promises in an array resolves, making it the opposite of Promise.all().
const fetchPromises = [fetch("/fast"), fetch("/slow")]const firstResolved = await Promise.any(fetchPromises)
console.log(firstResolved) // Logs the fastest resolved promiseNumeric separators _ make large numbers easier to read without affecting the actual value.
const budget = 1_000_000_000console.log(budget === 1000000000) // trueLogical assignment operators combine logical operations with assignment, simplifying common expressions.
let x = 1let y = 2x &&= yconsole.log(x) // 2 (x is reassigned if it’s truthy)You can now use await directly at the top level of your modules, making asynchronous operations even simpler.
await Promise.resolve(console.log("Hello from top-level await!"))Private class fields allow you to define private variables in a class using the # syntax, ensuring they’re inaccessible outside the class.
class Person { #firstName = Foo
showName() { console.log(`My name is ${this.#firstName}`) // Access private field within class }
#showAge() { console.log("Foo is 20") }}
const foo = new Person()console.log(foo.firstName) // undefinedStatic class fields and methods belong to the class itself rather than instances of the class. You can access them without creating an object of that class.
class Robot { static type = "Autonomous Machine" // Static field
constructor(name) { this.name = name }
static info() { return `Robot type: ${Robot.type}` // Static method }
introduce() { return `I am ${this.name}` }}
console.log(Robot.info()) // Output: Robot type: Autonomous Machine
const robot1 = new Robot("Cyberdyne Systems T-101")console.log(robot1.introduce()) // Output: I am Cyberdyne Systems T-101Static Fields and Methods on MDN
The Error.cause property allows you to include more context when throwing errors, especially when one error causes another. This helps with debugging by providing more detail.
const getUsers = async (array) => { try { const users = await fetch("<https://myapi/myusersfake>") return users } catch (error) { console.log("enter") throw new Error("Something when wrong, please try again later", { cause: error, }) }}
try { const users = await getUsers()} catch (error) { console.log(error.message) // Something went wrong, please try again later console.log(error.cause) // TypeError: Failed to fetch}The .at() method allows you to access elements from arrays and strings using negative indexing, making it easier to retrieve values from the end of a collection.
const fruitsArray = ["banana", "apple", "orange", "kiwi"]// Using positive and negative indexingconsole.log(fruitsArray.at(1)) // Output: appleconsole.log(fruitsArray.at(-1)) // Output: kiwi
const fruit = "kiwi"console.log(fruit.at(-1)) // Output: iThe Object.hasOwn() method provides a simpler way to check if an object has a specific property, replacing the more verbose hasOwnProperty.
const object = { foo: "bar" }
// Old way of checking property existenceif (Object.prototype.hasOwnProperty.call(object, "foo")) { console.log("The object has the property 'foo'")}
// Simplified with Object.hasOwn()if (Object.hasOwn(object, "foo")) { console.log("The object has the property 'foo'")}These methods allow you to search for the last element in an array that matches a condition, rather than the first one, making it easier to work with data from the end of an array.
const users = [ { id: 1, age: 15 }, { id: 2, age: 20 }, { id: 3, age: 22 },]
const lastUser = users.findLast((user) => user.age > 18)console.log(lastUser) // {id: 3, name: 'user3', age: 22}
const lastIndex = users.findLastIndex((user) => user.age > 18)console.log(lastIndex) // Output: 2Array.toReversed(), Array.toSorted(), Array.toSpliced()
Section titled “Array.toReversed(), Array.toSorted(), Array.toSpliced()”These new methods allow you to reverse, sort, or splice arrays without modifying the original array, providing a functional approach to array manipulation.
const prime = [13, 7, 17, 2]const sortPrime = prime.toSorted()
console.log(prime) // [13,7,17,2]; // Original array remains unchangedconsole.log(sortPrime) // [2,7,13,17];The with() method allows you to create a new array with one element updated, without changing the original array, making it a functional approach for immutability.
const usernames = ["user1", "user2", "user3"]
const updatedUsernames = usernames.with(1, "newUser")console.log(usernames) // ['user1', 'user2', 'user3'] - Original array remains unchangedconsole.log(updatedUsernames) // ['user1', 'newUser', 'user3']