JavaScript Modern Features (ES2020+)
JavaScript gets new features every year. This lesson covers the newer ones (from 2020 onwards) that you will see in modern code and job interviews.
Optional Chaining in Depth
The ES6 lesson showed ?. for properties. It also works for array items and method calls:
| Code | Means |
|---|---|
obj?.prop | Read prop only if obj is not null or undefined |
obj?.[key] | Same, with a key in brackets or an array index |
obj.method?.() | Call the method only if it exists |
Example
const order = {
id: 501,
items: [{ name: "Shalwar Kameez", price: 3500 }],
customer: null
};
console.log(order.items?.[0]?.name); // Shalwar Kameez
console.log(order.items?.[5]?.name); // undefined
console.log(order.customer?.phone); // undefined, no error
console.log(order.sendReceipt?.()); // undefined: no such methodThe whole chain stops at the first null or undefined and gives undefined, instead of crashing.
?? vs ||
Both give a fallback value, but they treat "empty" values differently:
Example
const settings = { volume: 0, nickname: "", theme: null };
console.log(settings.volume || 50); // 50 (0 is falsy, so it is replaced)
console.log(settings.volume ?? 50); // 0 (0 is a real value, kept)
console.log(settings.nickname ?? "Guest"); // "" (empty string kept)
console.log(settings.theme ?? "light"); // lightUse ?? when 0, "" or false are valid values. || replaces every falsy value, which can hide real data like a price of 0.
Logical Assignment
Three short operators combine a check with an assignment:
a ??= bgivesathe valuebonly ifais null or undefineda ||= bgivesathe valuebonly ifais falsya &&= bgivesathe valuebonly ifais truthy
Example
const profile = { name: "Zara" };
profile.city ??= "Karachi"; // added, because it was missing
profile.name ??= "Unknown"; // unchanged, it already exists
console.log(profile);{ name: 'Zara', city: 'Karachi' }Numeric Separators
Underscores make long numbers easy to read. JavaScript ignores them:
Example
const budget = 1_500_000; // fifteen lakh
console.log(budget + 1); // 1500001Array.prototype.at()
at(index) reads an item, and negative numbers count from the end. No more arr[arr.length - 1].
Example
const queue = ["Ali", "Sana", "Hamza", "Zainab"];
console.log(queue.at(0)); // Ali
console.log(queue.at(-1)); // Zainab (last)
console.log(queue.at(-2)); // HamzaStrings have at() too: "Karachi".at(-1) is "i".
Copying Array Methods
sort(), reverse() and splice() change the original array. Their newer partners return a new array and leave the original alone:
| Changes original | Returns a copy |
|---|---|
sort() | toSorted() |
reverse() | toReversed() |
splice() | toSpliced() |
arr[i] = x | with(i, x) |
Example
<!DOCTYPE html>
<html>
<body>
<p id="demo"></p>
<script>
const prices = [850, 120, 2400, 499];
const cheapFirst = prices.toSorted((a, b) => a - b);
const newestFirst = prices.toReversed();
const fixed = prices.with(1, 150);
document.getElementById("demo").innerHTML =
"Original: " + prices + "<br>" +
"toSorted: " + cheapFirst + "<br>" +
"toReversed: " + newestFirst + "<br>" +
"with(1, 150): " + fixed + "<br>" +
"Original is still: " + prices;
</script>
</body>
</html>Try it Yourself »Original: 850,120,2400,499 toSorted: 120,499,850,2400 toReversed: 499,2400,120,850 with(1, 150): 850,150,2400,499 Original is still: 850,120,2400,499
These are very useful in React and other frameworks, where you should not change data in place.
findLast and findLastIndex
Like find() and findIndex(), but they search from the end:
Example
const payments = [500, 1200, 300, 2000, 700];
console.log(payments.findLast(p => p > 1000)); // 2000
console.log(payments.findLastIndex(p => p > 1000)); // 3structuredClone: Real Deep Copies
The spread operator { ...obj } makes a shallow copy: objects inside are still shared. structuredClone() copies everything, at every level. It also handles Date, Map and Set.
Example
const original = {
name: "Ayesha",
marks: { maths: 80 },
joined: new Date(2026, 0, 10)
};
const shallow = { ...original };
const deep = structuredClone(original);
shallow.marks.maths = 95; // also changes original!
console.log(original.marks.maths); // 95
deep.marks.maths = 50; // original is safe
console.log(original.marks.maths); // still 95
console.log(deep.joined instanceof Date); // truestructuredClone cannot copy functions or DOM elements.
Object.groupBy
Object.groupBy(array, fn) sorts items into groups using the key your function returns:
Example
<!DOCTYPE html>
<html>
<body>
<p id="demo"></p>
<script>
const students = [
{ name: "Ali", course: "Python" },
{ name: "Sana", course: "Web" },
{ name: "Hamza", course: "Python" },
{ name: "Zara", course: "Web" },
{ name: "Imran", course: "AI" }
];
const byCourse = Object.groupBy(students, s => s.course);
let html = "";
for (const [course, list] of Object.entries(byCourse)) {
html += course + ": " + list.map(s => s.name).join(", ") + "<br>";
}
document.getElementById("demo").innerHTML = html;
</script>
</body>
</html>Try it Yourself »Python: Ali, Hamza Web: Sana, Zara AI: Imran
Map.groupBy() does the same but returns a Map.
More Handy Additions
str.replaceAll("a", "b")replaces every match, without a regexObject.hasOwn(obj, "key")checks if an object has its own propertyPromise.withResolvers()creates a promise together with itsresolveandrejectarr.flat()andarr.flatMap()flatten nested arrays
Exercise
Exercise
What does [10, 20, 30, 40].at(-1) return?
