ES2016 (ES7) — Small but Important
Array.prototype.includes
includes is cleaner than indexOf !== -1 and correctly handles NaN.
const fruits = ['apple', 'banana', 'cherry'];
// Old way: indexOffruits.indexOf('banana') !== -1; // truefruits.indexOf('grape') !== -1; // false
// New way: includesfruits.includes('banana'); // truefruits.includes('grape'); // false
// KEY ADVANTAGE: handles NaN correctly[1, 2, NaN].indexOf(NaN); // -1 — broken![1, 2, NaN].includes(NaN); // true ✅
// Second argument: start index[1, 2, 3, 2, 1].includes(2, 3); // starts searching from index 3 → falseExponentiation Operator
// Old wayMath.pow(2, 10); // 1024
// New way2 ** 10; // 10243 ** 3; // 272 ** 32; // 4294967296
// Assignment shorthandlet base = 2;base **= 8; // 256ES2017 (ES8) — async/await Changes Everything
async and await
async/await makes asynchronous code look and behave like synchronous code — no more .then() chains.
// ── Basic async/await ─────────────────────────────────────────────────async function fetchUserProfile(userId) { try { // await pauses this function until the Promise resolves const response = await fetch(`/api/users/${userId}`);
if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); }
const user = await response.json(); return user; } catch (error) { console.error('Failed to fetch user:', error); throw error; // re-throw so caller knows it failed }}
// ── Parallel async operations ─────────────────────────────────────────async function loadDashboard(userId) { // ❌ Sequential: slow — waits for each before starting next const user = await fetchUser(userId); const posts = await fetchPosts(userId); const stats = await fetchStats(userId);
// ✅ Parallel: fast — all three start at the same time const [user, posts, stats] = await Promise.all([ fetchUser(userId), fetchPosts(userId), fetchStats(userId), ]);
return { user, posts, stats };}
// ── async/await in loops ──────────────────────────────────────────────// Process items sequentially:async function processSequentially(items) { for (const item of items) { await processItem(item); // waits for each before proceeding }}
// Process items in parallel:async function processInParallel(items) { await Promise.all(items.map((item) => processItem(item)));}Object.entries and Object.values
const scores = { alice: 95, bob: 87, carol: 92 };
// Object.values: get all values as an arrayObject.values(scores); // [95, 87, 92]const average = Object.values(scores).reduce((a, b) => a + b) / Object.values(scores).length;
// Object.entries: get [key, value] pairsObject.entries(scores); // [['alice', 95], ['bob', 87], ['carol', 92]]
// Iterate over an object's key-value pairsfor (const [name, score] of Object.entries(scores)) { console.log(`${name}: ${score}`);}
// Convert object to Mapconst scoreMap = new Map(Object.entries(scores));
// Transform object valuesconst doubled = Object.fromEntries( Object.entries(scores).map(([key, val]) => [key, val * 2]),);// { alice: 190, bob: 174, carol: 184 }String Padding
// padStart: pad from the beginning'5'.padStart(3, '0'); // '005''42'.padStart(5, '0'); // '00042''hello'.padStart(10); // ' hello' (default pads with spaces)
// padEnd: pad from the end'hello'.padEnd(10); // 'hello ''3.14'.padEnd(10, '0'); // '3.14000000'
// Practical: format a list of items with aligned numbersconst items = ['apple', 'banana', 'cherry'];items.forEach((item, i) => { console.log(`${String(i + 1).padStart(2, '0')}. ${item}`);});// '01. apple'// '02. banana'// '03. cherry'ES2018 — Async Iteration and Object Rest/Spread
Object Rest and Spread (Finalized)
// ── Object rest in destructuring ──────────────────────────────────────const { a, b, ...remaining } = { a: 1, b: 2, c: 3, d: 4 };console.log(remaining); // { c: 3, d: 4 }
// Practical: remove a property non-destructivelyconst { password, ...safeUser } = { name: 'Alice', email: 'a@b.com', password: 'secret',};console.log(safeUser); // { name: 'Alice', email: 'a@b.com' }
// ── Object spread ─────────────────────────────────────────────────────const defaults = { theme: 'light', lang: 'en', timezone: 'UTC' };const overrides = { theme: 'dark', lang: 'fr' };const merged = { ...defaults, ...overrides }; // { theme: 'dark', lang: 'fr', timezone: 'UTC' }Promise.finally
// Always runs after a Promise settles — success or failure// Perfect for cleanup: hiding loading states, releasing resourcesfetch('/api/data') .then((res) => res.json()) .then((data) => render(data)) .catch((error) => showError(error)) .finally(() => { hideLoadingSpinner(); // runs regardless of success or failure });Regex Named Capture Groups
// Old: positional groups — confusing to maintainconst dateOld = /(\d{4})-(\d{2})-(\d{2})/;const matchOld = '2025-03-15'.match(dateOld);const year = matchOld[1]; // what is index 1 again?
// New: named capture groups — self-documentingconst date = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;const match = '2025-03-15'.match(date);const { year, month, day } = match.groups;// year: '2025', month: '03', day: '15'
// Named backreferencesconst repeat = /(?<word>\w+) \k<word>/; // matches "the the" but not "the cat"'the the'.match(repeat); // ✅ matches'the cat'.match(repeat); // ❌ no matchES2019 (ES10) — Quality of Life Improvements
Array.flat and Array.flatMap
// ── flat: flatten nested arrays ───────────────────────────────────────const nested = [1, [2, 3], [4, [5, 6]]];nested.flat(); // [1, 2, 3, 4, [5, 6]] — one level deep by defaultnested.flat(2); // [1, 2, 3, 4, 5, 6] — two levels deepnested.flat(Infinity); // fully flatten any depth
// ── flatMap: map then flat one level ──────────────────────────────────// More efficient than .map().flat() — single passconst sentences = ['Hello World', 'Foo Bar Baz'];const words = sentences.flatMap((sentence) => sentence.split(' '));// ['Hello', 'World', 'Foo', 'Bar', 'Baz']
// Practical: expand items from a listconst orders = [ { items: ['shirt', 'pants'] }, { items: ['shoes'] }, { items: ['hat', 'scarf', 'gloves'] },];const allItems = orders.flatMap((order) => order.items);// ['shirt', 'pants', 'shoes', 'hat', 'scarf', 'gloves']
// flatMap can also filter — return empty array to skipconst numbers = [1, 2, 3, 4, 5, 6];const doubledEvens = numbers.flatMap((n) => (n % 2 === 0 ? [n * 2] : []));// [4, 8, 12]Object.fromEntries
// The inverse of Object.entries()const entries = [ ['name', 'Alice'], ['age', 30], ['city', 'London'],];const obj = Object.fromEntries(entries);// { name: 'Alice', age: 30, city: 'London' }
// Transform an object's valuesconst prices = { apple: 1.5, banana: 0.5, cherry: 3.0 };const discounted = Object.fromEntries( Object.entries(prices).map(([fruit, price]) => [fruit, price * 0.9]),);
// Convert Map to objectconst map = new Map([ ['key1', 'val1'], ['key2', 'val2'],]);const fromMap = Object.fromEntries(map);String.trimStart and trimEnd
const padded = ' hello world ';padded.trimStart(); // 'hello world ' — remove leading whitespace onlypadded.trimEnd(); // ' hello world' — remove trailing whitespace onlypadded.trim(); // 'hello world' — both (existed before ES2019)
// Aliases for consistency (were previously trimLeft/trimRight which still work)Optional Catch Binding
// Before ES2019: had to write catch(error) even if you didn't use ittry { JSON.parse(maybeJson);} catch (error) { // error is unused return false;}
// ES2019: omit the binding when you don't need ittry { JSON.parse(maybeJson);} catch { return false; // no (error) needed}ES2020 (ES11) — Nullish Coalescing, Optional Chaining, and More
Optional Chaining ?.
One of the most beloved features in years. Stop writing defensive && chains.
const user = { name: 'Alice', address: { city: 'London', // no postcode }, getFullName: () => 'Alice Smith',};
// ── Old way: defensive checks ─────────────────────────────────────────const postcode = user && user.address && user.address.postcode;const city = user && user.address && user.address.city;
// ── New way: optional chaining ────────────────────────────────────────const postcode = user?.address?.postcode; // undefined (no error!)const city = user?.address?.city; // 'London'const zip = user?.address?.location?.zip; // undefined (no error even with 3 levels)
// ── With method calls ─────────────────────────────────────────────────const fullName = user?.getFullName?.(); // 'Alice Smith'const missing = user?.nonExistentMethod?.(); // undefined (no error!)
// ── With arrays ───────────────────────────────────────────────────────const tags = null;const firstTag = tags?.[0]; // undefined (no error!)
// ── Combined with nullish coalescing ──────────────────────────────────const city = user?.address?.city ?? 'Unknown city'; // 'London'const country = user?.address?.country ?? 'Unknown country'; // 'Unknown country'Nullish Coalescing ??
Returns the right side only when the left side is null or undefined (not falsy!).
// ── The problem with || for defaults ──────────────────────────────────const config = { timeout: 0, retries: 0, debug: false, name: '' };
// || treats 0, false, '' as falsy — uses wrong defaultconst timeout = config.timeout || 5000; // 5000 — WRONG! 0 is a valid timeoutconst debug = config.debug || true; // true — WRONG! false was intentionalconst name = config.name || 'App'; // 'App' — WRONG! '' was intentional
// ?? only triggers on null/undefinedconst timeout = config.timeout ?? 5000; // 0 ✅const debug = config.debug ?? true; // false ✅const name = config.name ?? 'App'; // '' ✅
// null and undefined DO trigger ??null ?? 'default'; // 'default'undefined ?? 'default'; // 'default'0 ?? 'default'; // 0'' ?? 'default'; // ''false ?? 'default'; // falseNullish Assignment ??=
let config = { timeout: null, retries: 0 };
// Only assign if the left side is null or undefinedconfig.timeout ??= 3000; // 3000 — was nullconfig.retries ??= 5; // 0 — retries is 0, not null/undefined, so unchanged
// Logical OR assignment ||=let value = 0;value ||= 42; // 42 — 0 is falsy, so assigns
// Logical AND assignment &&=let status = true;status &&= 'active'; // 'active' — true is truthy, so assigns
let empty = '';empty &&= 'content'; // '' — empty string is falsy, so does NOT assignBigInt — Arbitrary Precision Integers
// Regular numbers lose precision beyond 2^53 - 1Number.MAX_SAFE_INTEGER; // 90071992547409919007199254740991 + 1; // 90071992547409929007199254740991 + 2; // 9007199254740992 — wrong! ❌
// BigInt handles any size exactlyconst big = 9007199254740991n; // 'n' suffix makes it BigIntbig + 1n; // 9007199254740992n ✅big + 2n; // 9007199254740993n ✅
// Practical: cryptography, unique IDs from databases, financial systemsconst MAX_UINT64 = 18446744073709551615n;typeof 42n; // 'bigint'
// Cannot mix BigInt and Number directly42n + 1; // TypeError — must convert explicitly42n + BigInt(1); // 43n ✅Number(42n); // 42 (may lose precision for large values)globalThis — Universal Global Reference
// Before globalThis: different globals in different environments// Browser: window// Node.js: global// Web Workers: self
// The old dance:const theGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : undefined;
// Now: globalThis works everywhereglobalThis.setTimeout; // ✅ works in browser, Node.js, workers — everywhereglobalThis.fetch; // ✅ works in all modern environments