Array.at() and String.at()
The .at() method accepts positive and negative integers. Negative indices count from the end of the array or string. ES2022.
const fruits = ['apple', 'banana', 'cherry', 'date'];
// Positive index - same as bracket notation
console.log(fruits.at(0)); // 'apple'
console.log(fruits.at(1)); // 'banana'
// Negative index - counts from end
console.log(fruits.at(-1)); // 'date' (last)
console.log(fruits.at(-2)); // 'cherry' (second to last)
// Old way - verbose
console.log(fruits[fruits.length - 1]); // 'date'
// Out of range - returns undefined
console.log(fruits.at(10)); // undefined
console.log(fruits.at(-10)); // undefined
// Works on strings too
const str = 'Hello';
console.log(str.at(0)); // 'H'
console.log(str.at(-1)); // 'o'
// Works on TypedArrays
const bytes = new Uint8Array([0, 127, 255]);
console.log(bytes.at(-1)); // 255
// Useful in function return
function getLastItem(arr) {
return arr.at(-1); // much cleaner
}
// Use case: stack operations
const history = [];
history.push('/home', '/about', '/contact');
console.log(history.at(-1)); // '/contact' - current page
structuredClone()
The global structuredClone() creates a deep copy of an object using the structured clone algorithm. ES2022.
// Deep clone - nested objects are also copied
const original = {
name: 'Alice',
address: { city: 'Chennai', zip: '600001' },
scores: [95, 87, 91],
};
const clone = structuredClone(original);
clone.address.city = 'Mumbai'; // does not affect original
console.log(original.address.city); // 'Chennai'
// Types that structuredClone handles correctly
const complex = {
date: new Date('2024-01-15'), // preserved as Date object
map: new Map([['a', 1]]), // preserved as Map
set: new Set([1, 2, 3]), // preserved as Set
regex: /hello/gi, // preserved as RegExp
buffer: new ArrayBuffer(8), // preserved
undef: undefined, // preserved (JSON loses this)
};
const clone2 = structuredClone(complex);
console.log(clone2.date instanceof Date); // true (JSON gives a string)
console.log(clone2.map instanceof Map); // true
console.log(clone2.undef); // undefined (not lost)
// structuredClone DOES NOT handle:
// - Functions (throws DataCloneError)
// - DOM nodes (throws DataCloneError)
// - class instances (clones as plain object, loses methods)
// JSON.parse/stringify comparison
const bad = JSON.parse(JSON.stringify({
date: new Date(), // becomes a string
undef: undefined, // disappears entirely
inf: Infinity, // becomes null
nan: NaN, // becomes null
}));
console.log(bad.date instanceof Date); // false - it's a string now
Object.hasOwn()
A static method to safely check if an object has a property as its own (not inherited). ES2022 replacement for hasOwnProperty.
const user = { name: 'Alice', age: 30 };
// Object.hasOwn - preferred modern approach
console.log(Object.hasOwn(user, 'name')); // true
console.log(Object.hasOwn(user, 'age')); // true
console.log(Object.hasOwn(user, 'toString')); // false (inherited from prototype)
// Old approach - hasOwnProperty (still works, just less safe)
console.log(user.hasOwnProperty('name')); // true
// Why hasOwn is safer - works even when hasOwnProperty is overridden or absent
const nullProto = Object.create(null); // no prototype, no hasOwnProperty!
nullProto.key = 'value';
// nullProto.hasOwnProperty('key'); // TypeError: not a function
Object.hasOwn(nullProto, 'key'); // true - always works
// Works when hasOwnProperty is shadowed
const tricky = { hasOwnProperty: () => false }; // overrides the method
// tricky.hasOwnProperty('x'); // returns false even if property exists!
tricky.x = 1;
Object.hasOwn(tricky, 'x'); // true - not fooled
// Iterating own properties safely
const obj = { a: 1, b: 2 };
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
console.log(key, obj[key]); // only own properties
}
}
Promise.any()
Resolves with the first fulfilled promise. Rejects with an AggregateError only if ALL promises reject. ES2021.
// Resolves with fastest fulfillment, ignores individual failures
const fastest = await Promise.any([
fetch('https://cdn1.example.com/data.json'),
fetch('https://cdn2.example.com/data.json'), // might be faster
fetch('https://cdn3.example.com/data.json'),
]);
const data = await fastest.json();
// Only fails if ALL fail
try {
await Promise.any([
Promise.reject(new Error('CDN1 down')),
Promise.reject(new Error('CDN2 down')),
Promise.reject(new Error('CDN3 down')),
]);
} catch (err) {
console.log(err instanceof AggregateError); // true
console.log(err.errors.length); // 3 - all individual errors
err.errors.forEach(e => console.log(e.message));
}
// Comparison table:
// Promise.all - ALL must fulfill; first rejection = reject
// Promise.allSettled - wait for ALL; never rejects; status per result
// Promise.race - FIRST settled (fulfilled OR rejected) wins
// Promise.any - FIRST fulfilled wins; only fails if ALL reject
// Use case: try multiple data sources, use whichever responds first
async function fetchFromAnySource(endpoints) {
const results = await Promise.any(
endpoints.map(url => fetch(url).then(r => r.json()))
);
return results;
}
Object.groupBy() and Map.groupBy()
Group array elements by a computed key. Returns a plain object (or Map). ES2024.
const products = [
{ name: 'Laptop', category: 'Electronics', price: 999 },
{ name: 'Phone', category: 'Electronics', price: 699 },
{ name: 'Shirt', category: 'Clothing', price: 49 },
{ name: 'Jeans', category: 'Clothing', price: 79 },
{ name: 'Monitor', category: 'Electronics', price: 399 },
];
// Object.groupBy - groups into a plain object
const byCategory = Object.groupBy(products, p => p.category);
console.log(byCategory);
// {
// Electronics: [{ name: 'Laptop',...}, { name: 'Phone',...}, { name: 'Monitor',...}],
// Clothing: [{ name: 'Shirt',...}, { name: 'Jeans',...}]
// }
console.log(byCategory.Electronics.length); // 3
// Group by price range
const byPriceRange = Object.groupBy(products, p => {
if (p.price < 100) return 'budget';
if (p.price < 500) return 'mid-range';
return 'premium';
});
// Map.groupBy - same but uses a Map (supports non-string keys)
const byPrice = Map.groupBy(products, p => Math.floor(p.price / 100) * 100);
console.log(byPrice.get(900)); // [{ name: 'Laptop',... }]
// Before Object.groupBy - using reduce
const manualGroup = products.reduce((acc, p) => {
(acc[p.category] ??= []).push(p);
return acc;
}, {});
Logical Assignment Operators
Combine logical operators with assignment. Only assigns if the condition holds. ES2021.
// ??= - assign only if left side is null or undefined
let config = null;
config ??= { theme: 'dark' }; // assigned - config was null
config ??= { theme: 'light' }; // NOT assigned - config already has a value
console.log(config.theme); // 'dark'
// Practical: initialize default values
function processUser(user) {
user.name ??= 'Anonymous';
user.role ??= 'viewer';
user.prefs ??= {};
user.prefs.theme ??= 'system';
}
// ||= - assign only if left side is falsy (null, undefined, 0, '', false)
let count = 0;
count ||= 10; // assigned - 0 is falsy
console.log(count); // 10
let title = 'My Title';
title ||= 'Default'; // NOT assigned - 'My Title' is truthy
console.log(title); // 'My Title'
// &&= - assign only if left side is truthy
let user = { name: 'Alice' };
user &&= { ...user, active: true }; // assigned - user is truthy
console.log(user); // { name: 'Alice', active: true }
let guestUser = null;
guestUser &&= { ...guestUser, active: true }; // NOT assigned - null is falsy
console.log(guestUser); // null (unchanged)
// Comparison:
// a ??= b is a ?? (a = b) - only null/undefined
// a ||= b is a || (a = b) - any falsy value
// a &&= b is a && (a = b) - truthy condition
Nullish Coalescing and Optional Chaining
Essential modern syntax for safely handling null and undefined. ES2020.
// ?? - nullish coalescing: use right side only for null/undefined
const port = process.env.PORT ?? 3000;
const name = user.name ?? 'Guest'; // '' stays '', null/undefined -> 'Guest'
const count = settings.count ?? 0; // 0 stays 0, null/undefined -> 0
// vs || which treats ALL falsy as missing (0, '', false are lost)
const port2 = process.env.PORT || 3000; // 0 would become 3000 - bug!
// ?. - optional chaining: stop at null/undefined instead of throwing
const street = user?.address?.street; // undefined if any step is null/undefined
const first = arr?.[0]; // undefined if arr is null/undefined
const result = obj?.method?.(); // undefined if method doesn't exist or obj is nullish
// Old way - verbose null checks
const old = user && user.address && user.address.street;
// Combining ?? and ?.
const city = user?.address?.city ?? 'Unknown';
const zip = user?.address?.zip?.replace('-', '') ?? 'N/A';
// ?. in method calls
const len = str?.length; // fine on any string or null/undefined
str?.trim()?.split(' '); // chain method calls safely
// ?. with delete and assignment (returns undefined, doesn't throw)
delete user?.address?.street; // no error even if user is null
// Practical: API response handling
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return {
name: data?.profile?.displayName ?? data?.username ?? 'Unknown',
avatar: data?.profile?.avatar?.url ?? '/default-avatar.png',
role: data?.roles?.at(0) ?? 'member',
};
}
More ES2024 Additions
// Promise.withResolvers() - ES2024
// Create a Promise and expose its resolve/reject externally
const { promise, resolve, reject } = Promise.withResolvers();
// Old way:
// let resolve, reject;
// const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
setTimeout(() => resolve('done'), 1000);
const result = await promise; // 'done'
// Useful for deferred patterns
function createDeferred() {
return Promise.withResolvers();
}
const deferred = createDeferred();
element.addEventListener('click', deferred.resolve, { once: true });
await deferred.promise; // wait for click
// Array.fromAsync() - ES2024 - like Array.from() but for async iterables
const rows = await Array.fromAsync(db.query('SELECT * FROM users'));
// vs old: const rows = []; for await (const row of cursor) rows.push(row);
// Well-formed Unicode strings - ES2024
const lone = '\uD800'; // lone surrogate (not valid Unicode)
console.log(lone.isWellFormed()); // false
console.log(lone.toWellFormed()); // replacement character
console.log('hello'.isWellFormed()); // true
// Temporal (proposal, not yet standard - but coming soon)
// const now = Temporal.Now.plainDateTimeISO();
// Much better than the Date object for complex date math
// Object.fromEntries - ES2019 (pairs well with modern features)
const entries = [['name', 'Alice'], ['age', 30]];
const obj = Object.fromEntries(entries); // { name: 'Alice', age: 30 }
// Transform object values via entries
const prices = { apple: 1.5, banana: 0.5, cherry: 2.0 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);