ES6+ Features

45 min read Intermediate

Master modern JavaScript features from ES6 (2015) through ES2023: destructuring, spread operator, optional chaining, modules, iterators, and more.

Advanced Destructuring

Extract values from arrays and objects with powerful patterns.

JavaScript
// Nested object destructuring
const user = {
    name: "John",
    address: {
        city: "New York",
        country: "USA",
        coords: { lat: 40.7, lng: -74.0 }
    },
    contacts: {
        email: "john@example.com",
        phone: "123-456-7890"
    }
};

const {
    name,
    address: { city, coords: { lat, lng } },
    contacts: { email }
} = user;

console.log(city, lat, lng, email);
// "New York", 40.7, -74.0, "john@example.com"

// Default values with renaming
const { name: userName = "Anonymous", age = 25 } = user;

// Array destructuring with skipping
const colors = ["red", "green", "blue", "yellow"];
const [primary, , tertiary] = colors;
console.log(primary, tertiary); // "red", "blue"

// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1

// Rest in destructuring
const [first, second, ...remaining] = colors;
console.log(remaining); // ["blue", "yellow"]

const { name: n, ...otherProps } = user;
console.log(otherProps); // { address: {...}, contacts: {...} }

// Function parameter destructuring
function createUser({ name, email, role = "user" }) {
    return { name, email, role, createdAt: Date.now() };
}

createUser({ name: "Jane", email: "jane@example.com" });

// Complex destructuring in function
function processOrder({ 
    id,
    items = [],
    customer: { name, address: { city } = {} } = {}
} = {}) {
    console.log(id, name, city, items.length);
}

// Destructuring in loops
const users = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" }
];

for (const { id, name } of users) {
    console.log(`${id}: ${name}`);
}

Spread and Rest Operators

Use ... for spreading and collecting values.

JavaScript
// Array spread
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Clone array
const clone = [...arr1];

// Insert in middle
const withInsert = [0, ...arr1, 4, 5]; // [0, 1, 2, 3, 4, 5]

// Object spread
const defaults = { theme: "light", language: "en" };
const userPrefs = { theme: "dark" };
const settings = { ...defaults, ...userPrefs };
// { theme: "dark", language: "en" }

// Clone with modifications
const updatedUser = { ...user, name: "Jane", age: 26 };

// Shallow clone (nested objects are references!)
const original = { nested: { value: 1 } };
const copy = { ...original };
copy.nested.value = 2;
console.log(original.nested.value); // 2 (modified!)

// Deep clone
const deepClone = JSON.parse(JSON.stringify(original));
// Or: structuredClone(original) (modern browsers)

// Rest parameters (collect remaining arguments)
function sum(...numbers) {
    return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

function logFirst(first, ...rest) {
    console.log("First:", first);
    console.log("Rest:", rest);
}
logFirst(1, 2, 3, 4);
// First: 1
// Rest: [2, 3, 4]

// Spread in function calls
const numbers = [5, 10, 15];
console.log(Math.max(...numbers)); // 15

// Converting iterables
const str = "hello";
const chars = [...str]; // ["h", "e", "l", "l", "o"]

const set = new Set([1, 2, 3]);
const setArray = [...set]; // [1, 2, 3]

// Practical: Merge arrays with deduplication
const merged = [...new Set([...arr1, ...arr2])];

Optional Chaining & Nullish Coalescing

Handle undefined/null values safely and elegantly.

JavaScript
// Optional chaining (?.)
const user = {
    name: "John",
    address: null
};

// Without optional chaining
const city = user && user.address && user.address.city;

// With optional chaining
const cityOptional = user?.address?.city;
console.log(cityOptional); // undefined (no error!)

// With arrays
const users = null;
const firstUser = users?.[0];
const firstName = users?.[0]?.name;

// With methods
const result = user.getName?.();
const length = user.items?.length;

// With function calls
const callback = null;
callback?.(); // Does nothing, no error

// Nullish coalescing (??)
// Returns right side only if left is null or undefined
const value = null ?? "default";      // "default"
const value2 = undefined ?? "default"; // "default"
const value3 = 0 ?? "default";         // 0 (0 is NOT nullish!)
const value4 = "" ?? "default";        // "" (empty string is NOT nullish!)
const value5 = false ?? "default";     // false (false is NOT nullish!)

// Compare with OR (||)
// || returns right side for ANY falsy value
const orValue = 0 || "default";        // "default" (0 is falsy)
const orValue2 = "" || "default";      // "default" (empty string is falsy)

// Practical example
function getConfig(options = {}) {
    return {
        port: options.port ?? 3000,
        host: options.host ?? "localhost",
        debug: options.debug ?? false
    };
}

getConfig({ port: 8080 });
// { port: 8080, host: "localhost", debug: false }

// Combining both
const settings = user?.settings?.theme ?? "light";

// Assignment operators (ES2021)
let config = null;
config ??= { default: true }; // Assign if null/undefined

let count = 0;
count ||= 10;  // Assign if falsy (count stays 0? No, becomes 10!)
count &&= 5;   // Assign if truthy
When to Use Which

Use ?? when you only want to check for null/undefined. Use || when you want to check for any falsy value (0, "", false, null, undefined).

Symbols & Iterators

Create unique identifiers and custom iterable objects.

JavaScript
// Symbols - unique identifiers
const sym1 = Symbol("description");
const sym2 = Symbol("description");
console.log(sym1 === sym2); // false (always unique)

// Use as object keys
const ID = Symbol("id");
const user = {
    [ID]: 123,
    name: "John"
};

console.log(user[ID]); // 123
console.log(Object.keys(user)); // ["name"] - symbols are hidden

// Well-known symbols
class Collection {
    constructor(items) {
        this.items = items;
    }
    
    // Make it iterable
    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;
        
        return {
            next() {
                if (index < items.length) {
                    return { value: items[index++], done: false };
                }
                return { done: true };
            }
        };
    }
    
    // Custom string representation
    [Symbol.toStringTag] = "Collection";
}

const collection = new Collection([1, 2, 3]);
for (const item of collection) {
    console.log(item); // 1, 2, 3
}

console.log(collection.toString()); // "[object Collection]"

// Generator functions (easier iterators)
function* numberGenerator(start, end) {
    for (let i = start; i <= end; i++) {
        yield i;
    }
}

const gen = numberGenerator(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log([...gen]);   // [2, 3, 4, 5] (remaining values)

// Infinite generator
function* infiniteSequence() {
    let n = 0;
    while (true) {
        yield n++;
    }
}

const infinite = infiniteSequence();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
// ...

// Practical: Paginated data
function* paginate(items, pageSize) {
    for (let i = 0; i < items.length; i += pageSize) {
        yield items.slice(i, i + pageSize);
    }
}

const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (const page of paginate(data, 3)) {
    console.log("Page:", page);
}
// Page: [1, 2, 3]
// Page: [4, 5, 6]
// Page: [7, 8, 9]
// Page: [10]

Map and Set

Specialized collections for key-value pairs and unique values.

JavaScript
// Map - key-value pairs with any key type
const map = new Map();

// Any value can be a key
map.set("string", "value1");
map.set(42, "value2");
map.set({ id: 1 }, "value3");

const objKey = { id: 2 };
map.set(objKey, "value4");

console.log(map.get("string")); // "value1"
console.log(map.get(objKey));   // "value4"
console.log(map.has(42));       // true
console.log(map.size);          // 4

map.delete("string");
// map.clear() - remove all

// Initialize with entries
const userMap = new Map([
    ["user1", { name: "Alice" }],
    ["user2", { name: "Bob" }]
]);

// Iterating
for (const [key, value] of map) {
    console.log(key, value);
}

map.forEach((value, key) => {
    console.log(key, value);
});

console.log([...map.keys()]);   // All keys
console.log([...map.values()]); // All values
console.log([...map.entries()]); // All [key, value] pairs

// Set - unique values only
const set = new Set([1, 2, 3, 3, 3]);
console.log(set.size); // 3 (duplicates removed)

set.add(4);
set.add(1); // Already exists, ignored
set.delete(2);
set.has(3); // true

// Array deduplication
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]

// Set operations
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// Union
const union = new Set([...setA, ...setB]);
// {1, 2, 3, 4, 5, 6}

// Intersection
const intersection = new Set([...setA].filter(x => setB.has(x)));
// {3, 4}

// Difference
const difference = new Set([...setA].filter(x => !setB.has(x)));
// {1, 2}

// WeakMap & WeakSet (garbage-collectible keys)
const weakMap = new WeakMap();
let obj = { data: "important" };
weakMap.set(obj, "metadata");

obj = null; // Object can be garbage collected
// Entry in weakMap is also removed

Modern Features (ES2020-2023)

JavaScript
// BigInt (ES2020) - arbitrary precision integers
const bigNumber = 9007199254740991n; // Note the 'n'
const bigNumber2 = BigInt("9007199254740991");
console.log(bigNumber + 1n); // 9007199254740992n

// globalThis (ES2020) - universal global object
console.log(globalThis); // window in browser, global in Node

// Promise.allSettled (ES2020)
const promises = [
    Promise.resolve(1),
    Promise.reject("error"),
    Promise.resolve(3)
];

const results = await Promise.allSettled(promises);
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected", reason: "error" },
//   { status: "fulfilled", value: 3 }
// ]

// String replaceAll (ES2021)
const text = "foo bar foo baz foo";
console.log(text.replaceAll("foo", "qux"));
// "qux bar qux baz qux"

// Numeric separators (ES2021)
const billion = 1_000_000_000;
const bytes = 0xFF_FF_FF_FF;
const bits = 0b1010_0001_1000_0101;

// Array at() (ES2022) - negative indexing
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(-1));  // 5 (last element)
console.log(arr.at(-2));  // 4 (second to last)

// Object.hasOwn (ES2022)
const obj = { prop: "value" };
console.log(Object.hasOwn(obj, "prop")); // true
// Better than obj.hasOwnProperty("prop")

// Error cause (ES2022)
try {
    throw new Error("High level error", {
        cause: new Error("Root cause")
    });
} catch (e) {
    console.log(e.cause); // Error: Root cause
}

// Top-level await (ES2022) - in modules
const data = await fetch("/api/data").then(r => r.json());

// Array findLast/findLastIndex (ES2023)
const nums = [1, 2, 3, 4, 3, 2, 1];
console.log(nums.findLast(n => n > 2));      // 3 (last match)
console.log(nums.findLastIndex(n => n > 2)); // 4 (index of last match)

// Array toSorted, toReversed, toSpliced, with (ES2023)
// Non-mutating versions of array methods
const original = [3, 1, 2];
const sorted = original.toSorted();  // [1, 2, 3]
console.log(original);               // [3, 1, 2] (unchanged!)

const reversed = original.toReversed(); // [2, 1, 3]
const withChange = original.with(0, 9); // [9, 1, 2]
const spliced = original.toSpliced(1, 1, 5); // [3, 5, 2]
Browser Support

Modern features require modern browsers. Use tools like Babel to transpile for older browser support, or check compatibility on caniuse.com.

Private Class Fields & Methods

ES2022 introduced true private fields and methods using the # prefix. Unlike the underscore convention, these are enforced by the language.

Private Fields

JavaScript - Private Class Members
class BankAccount {
    // Private fields (must be declared at class level)
    #balance = 0;
    #transactions = [];
    #pin;
    
    // Static private field
    static #totalAccounts = 0;
    
    constructor(initialBalance, pin) {
        this.#balance = initialBalance;
        this.#pin = pin;
        BankAccount.#totalAccounts++;
    }
    
    // Private method
    #validatePin(pin) {
        return this.#pin === pin;
    }
    
    // Private getter
    get #formattedBalance() {
        return `$${this.#balance.toFixed(2)}`;
    }
    
    // Public methods accessing private fields
    deposit(amount, pin) {
        if (!this.#validatePin(pin)) {
            throw new Error('Invalid PIN');
        }
        this.#balance += amount;
        this.#transactions.push({ type: 'deposit', amount, date: new Date() });
        return this.#formattedBalance;
    }
    
    withdraw(amount, pin) {
        if (!this.#validatePin(pin)) throw new Error('Invalid PIN');
        if (amount > this.#balance) throw new Error('Insufficient funds');
        
        this.#balance -= amount;
        this.#transactions.push({ type: 'withdraw', amount, date: new Date() });
        return this.#formattedBalance;
    }
    
    getBalance(pin) {
        if (!this.#validatePin(pin)) throw new Error('Invalid PIN');
        return this.#formattedBalance;
    }
    
    // Static private method
    static #generateAccountNumber() {
        return Math.random().toString(36).substr(2, 9).toUpperCase();
    }
    
    static getAccountCount() {
        return BankAccount.#totalAccounts;
    }
}

const account = new BankAccount(1000, '1234');
account.deposit(500, '1234'); // "$1500.00"

// These fail:
// account.#balance         // SyntaxError: Private field
// account['#balance']      // undefined (not the same thing)
// account.#validatePin()   // SyntaxError

Private Field Checks

JavaScript - in Operator with Private Fields
// ES2022: Check if object has private field (ergonomic brand check)
class MyClass {
    #privateField = 42;
    
    static isMyClass(obj) {
        // Can check if obj has the private field
        return #privateField in obj;
    }
    
    // Useful for ensuring methods are called on correct instances
    doSomething() {
        if (!(#privateField in this)) {
            throw new TypeError('Invalid receiver');
        }
        return this.#privateField;
    }
}

const instance = new MyClass();
const fake = { doSomething: instance.doSomething };

MyClass.isMyClass(instance); // true
MyClass.isMyClass(fake);     // false
MyClass.isMyClass({});       // false

ES2020 Features

ECMAScript 2020 brought optional chaining, nullish coalescing, BigInt, and more significant improvements.

JavaScript - ES2020 Features
// Optional Chaining (?.)
const user = { 
    profile: { 
        address: { city: 'NYC' } 
    } 
};
const city = user?.profile?.address?.city; // 'NYC'
const zip = user?.profile?.address?.zip;   // undefined (no error)

// With methods
const result = obj?.method?.();  // Call if method exists

// With arrays
const first = arr?.[0];          // Safe array access

// With dynamic properties
const prop = obj?.[dynamicKey];

// Nullish Coalescing (??)
// Only falls back for null/undefined (not falsy values!)
const value = null ?? 'default';   // 'default'
const zero = 0 ?? 'default';       // 0 (0 is not nullish)
const empty = '' ?? 'default';     // '' (empty string is not nullish)
const falsy = false ?? 'default';  // false

// Combine with optional chaining
const name = user?.profile?.name ?? 'Anonymous';

// Logical assignment operators (ES2021)
let a = null;
a ??= 'default';  // a = 'default' (only if a is nullish)

let b = '';
b ||= 'default';  // b = 'default' (if b is falsy)

let c = 0;
c &&= 10;         // c = 0 (only assigns if c is truthy)

// BigInt - arbitrary precision integers
const big = 9007199254740991n;  // 'n' suffix makes it BigInt
const alsoBig = BigInt('9007199254740991');

console.log(big + 1n);     // 9007199254740992n (correct!)
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992 (unsafe!)

// Can't mix BigInt with regular numbers
// big + 1  // TypeError!
// Must convert explicitly
const mixed = big + BigInt(1);

// Promise.allSettled - never rejects
const results = await Promise.allSettled([
    fetch('/api/users'),
    fetch('/api/posts'),
    fetch('/api/comments')
]);

results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
        console.log(`Request ${i} succeeded:`, result.value);
    } else {
        console.log(`Request ${i} failed:`, result.reason);
    }
});

// globalThis - consistent global object
globalThis.myGlobal = 42;  // Works in browser, Node, workers, etc.

ES2021-2024 Features

JavaScript - ES2021-2024 Features
// String.prototype.replaceAll (ES2021)
const str = 'foo bar foo';
str.replaceAll('foo', 'baz'); // 'baz bar baz'

// Numeric separators (ES2021)
const billion = 1_000_000_000;
const bytes = 0xFF_FF_FF_FF;
const binary = 0b1010_0001_1000_0101;

// WeakRef and FinalizationRegistry (ES2021)
// For advanced memory management
const weakRef = new WeakRef(largeObject);
weakRef.deref(); // Returns object or undefined if collected

// Promise.any (ES2021) - first to succeed
try {
    const first = await Promise.any([
        fetch('/fast-server'),
        fetch('/slow-server'),
        fetch('/backup-server')
    ]);
    console.log('First successful:', first);
} catch (e) {
    console.log('All failed:', e.errors);
}

// Object.hasOwn (ES2022) - safer than hasOwnProperty
const obj = { name: 'Alice' };
Object.hasOwn(obj, 'name');     // true
Object.hasOwn(obj, 'toString'); // false (inherited)

// Works with objects that have null prototype
const nullProto = Object.create(null);
nullProto.key = 'value';
Object.hasOwn(nullProto, 'key'); // true
// nullProto.hasOwnProperty('key') // Error! No such method

// Array.prototype.at (ES2022) - negative indexing
const arr = ['a', 'b', 'c', 'd'];
arr.at(-1);  // 'd' (last element)
arr.at(-2);  // 'c' (second to last)
arr.at(0);   // 'a'

// Also works on strings
'hello'.at(-1); // 'o'

// RegExp match indices (ES2022)
const regex = /(?\w+)/d;  // 'd' flag for indices
const match = regex.exec('hello world');
match.indices[0];        // [0, 5] - start and end of match
match.indices.groups.word; // [0, 5] - named group indices

// Error cause (ES2022)
try {
    await fetch('/api/data');
} catch (err) {
    throw new Error('Failed to load data', { cause: err });
}

// Array methods: toSorted, toReversed, toSpliced, with (ES2023)
const numbers = [3, 1, 2];
numbers.toSorted();           // [1, 2, 3] - original unchanged
numbers.toReversed();         // [2, 1, 3] - original unchanged
numbers.toSpliced(1, 1, 99);  // [3, 99, 2] - original unchanged
numbers.with(0, 10);          // [10, 1, 2] - original unchanged

// findLast / findLastIndex (ES2023)
const data = [1, 2, 3, 4, 3, 2];
data.findLast(x => x > 2);      // 3 (last match)
data.findLastIndex(x => x > 2); // 4 (index of last match)

// Hashbang grammar (ES2023)
// #!/usr/bin/env node
// console.log('Script executed directly');

// Symbols as WeakMap keys (ES2023)
const symbolKey = Symbol('metadata');
const weakMap = new WeakMap();
weakMap.set(symbolKey, { data: 'value' });

// Promise.withResolvers (ES2024)
const { promise, resolve, reject } = Promise.withResolvers();
// Later...
resolve('done!');

// Object.groupBy (ES2024)
const items = [
    { type: 'fruit', name: 'apple' },
    { type: 'vegetable', name: 'carrot' },
    { type: 'fruit', name: 'banana' }
];
const grouped = Object.groupBy(items, item => item.type);
// { fruit: [...], vegetable: [...] }

// Map.groupBy (ES2024)
const mapGrouped = Map.groupBy(items, item => item.type);
// Map { 'fruit' => [...], 'vegetable' => [...] }
Staying Current
  • TC39 proposals: Follow github.com/tc39/proposals for upcoming features
  • Stage 4: Features at stage 4 will be in the next ECMAScript version
  • Browser support: Check caniuse.com or MDN for compatibility
  • Transpilers: Use Babel or TypeScript for older browser support

Summary

Destructuring

Extract values from objects and arrays

Spread/Rest

... for expanding and collecting values

Optional Chaining

?. for safe property access

Nullish Coalescing

?? for null/undefined defaults

Map/Set

Specialized collections

Generators

function* for custom iterators