Back to Articles

Chapter 14: Modern JavaScript Features (ES6+)

July 27, 202514 min read
javascriptes6es2020optional-chainingnullish-coalescingproxyreflectmodern
Chapter 14: Modern JavaScript Features (ES6+)

Chapter 14: Modern JavaScript Features (ES6+)

Destructuring with Default Values and Renaming

// Complex destructuring scenarios
const config = {
    api: {
        baseUrl: "https://api.example.com",
        timeout: 5000
    },
    features: {
        darkMode: true
    }
};

// Nested destructuring with defaults and renaming
const {
    api: { 
        baseUrl: apiUrl = "https://default.com",
        timeout = 3000,
        retries = 3 // Not in original object
    },
    features: { 
        darkMode = false,
        notifications: enableNotifications = true 
    } = {} // Default to empty object if features is undefined
} = config;

console.log(apiUrl, timeout, retries, darkMode, enableNotifications);

Advanced Array Methods

const users = [
    { id: 1, name: "Alice", age: 25, active: true },
    { id: 2, name: "Bob", age: 30, active: false },
    { id: 3, name: "Charlie", age: 35, active: true },
    { id: 4, name: "Diana", age: 28, active: true }
];

// Chaining array methods
const result = users
    .filter(user => user.active)                    // Only active users
    .map(user => ({ ...user, ageGroup: user.age < 30 ? 'young' : 'mature' }))
    .sort((a, b) => a.age - b.age)                  // Sort by age
    .reduce((acc, user) => {                        // Group by age group
        const group = user.ageGroup;
        acc[group] = acc[group] || [];
        acc[group].push(user);
        return acc;
    }, {});

console.log(result);

// flatMap - map and flatten in one step
const nested = [[1, 2], [3, 4], [5, 6]];
const flattened = nested.flatMap(arr => arr.map(x => x * 2));
console.log(flattened); // [2, 4, 6, 8, 10, 12]

// Array.from with mapping function
const range = Array.from({ length: 5 }, (_, i) => i * 2);
console.log(range); // [0, 2, 4, 6, 8]

Proxy and Reflect

// Proxy - intercept and customize operations
const user = {
    name: "Alice",
    age: 30
};

const userProxy = new Proxy(user, {
    get(target, property) {
        console.log(`Getting ${property}`);
        return target[property];
    },
    
    set(target, property, value) {
        console.log(`Setting ${property} to ${value}`);
        if (property === 'age' && value < 0) {
            throw new Error("Age cannot be negative");
        }
        target[property] = value;
        return true;
    },
    
    has(target, property) {
        console.log(`Checking if ${property} exists`);
        return property in target;
    }
});

console.log(userProxy.name);  // "Getting name" → "Alice"
userProxy.age = 31;           // "Setting age to 31"
console.log('name' in userProxy); // "Checking if name exists" → true

// Reflect - programmatic object operations
const obj = { a: 1, b: 2 };
console.log(Reflect.has(obj, 'a'));           // true
console.log(Reflect.ownKeys(obj));            // ["a", "b"]
Reflect.set(obj, 'c', 3);
console.log(obj);                             // { a: 1, b: 2, c: 3 }

WeakMap and WeakSet

// WeakMap - weak references to objects as keys
const privateData = new WeakMap();

class BankAccount {
    constructor(balance) {
        // Store private data using WeakMap
        privateData.set(this, { balance, transactions: [] });
    }
    
    deposit(amount) {
        const data = privateData.get(this);
        data.balance += amount;
        data.transactions.push(`Deposited ${amount}`);
        return data.balance;
    }
    
    getBalance() {
        return privateData.get(this).balance;
    }
    
    getTransactions() {
        return privateData.get(this).transactions.slice(); // Return copy
    }
}

const account = new BankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150

// Private data is truly private - no way to access it directly
console.log(account.balance); // undefined

// WeakSet - weak references to objects
const visitedNodes = new WeakSet();

function traverse(node) {
    if (visitedNodes.has(node)) {
        return; // Avoid cycles
    }
    
    visitedNodes.add(node);
    // Process node...
    
    if (node.children) {
        node.children.forEach(child => traverse(child));
    }
}

Optional Chaining and Nullish Coalescing

// Optional chaining (?.) - safely access nested properties
const user = {
    profile: {
        social: {
            twitter: "@alice"
        }
    }
};

// Traditional way (verbose and error-prone)
const twitter1 = user && user.profile && user.profile.social && user.profile.social.twitter;

// Optional chaining (clean and safe)
const twitter2 = user?.profile?.social?.twitter;
const instagram = user?.profile?.social?.instagram; // undefined (no error)

// Works with arrays and function calls too
const firstHobby = user?.hobbies?.[0];
const result = user?.getName?.();

// Nullish coalescing (??) - default values for null/undefined
const username = user?.name ?? "Anonymous";
const theme = user?.preferences?.theme ?? "light";

// Different from || operator
console.log("" || "default");  // "default" (empty string is falsy)
console.log("" ?? "default");  // "" (empty string is not nullish)
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"

Dynamic Imports and Top-level Await

// Dynamic imports - load modules conditionally
async function loadModule(condition) {
    if (condition) {
        const module = await import('./heavy-module.js');
        return module.default();
    }
}

// Top-level await (in modules)
// utils.js
const data = await fetch('https://api.example.com/data').then(r => r.json());
export { data };

// Code splitting with dynamic imports
const LazyComponent = lazy(() => import('./LazyComponent'));

Logical Assignment Operators

// Logical assignment operators (ES2021)
let a = null;
let b = 0;
let c = "";

// Nullish coalescing assignment
a ??= "default"; // Only assigns if a is null or undefined
console.log(a); // "default"

// Logical AND assignment
b &&= 42; // Only assigns if b is truthy
console.log(b); // 0 (not assigned because 0 is falsy)

// Logical OR assignment
c ||= "fallback"; // Only assigns if c is falsy
console.log(c); // "fallback"

// Practical example
class Config {
    constructor(options = {}) {
        this.debug = options.debug ?? false;
        this.timeout = options.timeout ?? 5000;
        this.retries = options.retries ?? 3;
        
        // Set default headers
        this.headers ??= {};
        this.headers['Content-Type'] ??= 'application/json';
        
        // Enable caching if not explicitly disabled
        this.cache &&= true;
    }
}

String Methods and Pattern Matching

// String padding
const num = "42";
console.log(num.padStart(5, "0")); // "00042"
console.log(num.padEnd(5, "!"));   // "42!!!"

// String matching
const text = "Hello World JavaScript";

// includes, startsWith, endsWith
console.log(text.includes("World"));     // true
console.log(text.startsWith("Hello"));   // true
console.log(text.endsWith("Script"));    // true

// replaceAll (ES2021)
const sentence = "The quick brown fox jumps over the lazy dog";
console.log(sentence.replaceAll("the", "THE")); // Replaces all occurrences

// matchAll for global regex matches
const regex = /\b\w{4}\b/g; // 4-letter words
const matches = [...sentence.matchAll(regex)];
console.log(matches.map(match => match[0])); // ["over", "lazy"]

BigInt for Large Numbers

// BigInt for numbers larger than Number.MAX_SAFE_INTEGER
const bigNumber = 9007199254740991n; // Note the 'n' suffix
const anotherBig = BigInt("9007199254740992");

console.log(bigNumber + 1n); // 9007199254740992n
console.log(bigNumber * 2n); // 18014398509481982n

// Can't mix BigInt with regular numbers
// console.log(bigNumber + 1); // TypeError!

// Convert between BigInt and Number
console.log(Number(bigNumber)); // May lose precision
console.log(BigInt(123));       // 123n

// Use cases: cryptography, precise calculations
function factorial(n) {
    if (n <= 1n) return 1n;
    return n * factorial(n - 1n);
}

console.log(factorial(20n)); // 2432902008176640000n

Private Class Fields

// Private fields and methods (ES2022)
class ModernUser {
    // Private fields
    #id;
    #password;
    
    // Private static field
    static #instanceCount = 0;
    
    constructor(username, password) {
        this.username = username;
        this.#password = this.#hashPassword(password);
        this.#id = ++ModernUser.#instanceCount;
    }
    
    // Private method
    #hashPassword(password) {
        return `hashed_${password}`;
    }
    
    // Public method using private fields
    authenticate(password) {
        return this.#hashPassword(password) === this.#password;
    }
    
    getId() {
        return this.#id;
    }
    
    // Static method using private static field
    static getTotalUsers() {
        return ModernUser.#instanceCount;
    }
}

const user = new ModernUser("alice", "secret123");
console.log(user.authenticate("secret123")); // true
console.log(user.getId()); // 1
// console.log(user.#id); // SyntaxError: Private field '#id' must be declared in an enclosing class

Numeric Separators and Other Literals

// Numeric separators for readability
const million = 1_000_000;
const binary = 0b1010_0001;
const octal = 0o755_000;
const hex = 0xFF_EC_DE_5E;

console.log(million); // 1000000

// Template literal improvements
const createQuery = (table, fields) => `
    SELECT ${fields.join(', ')}
    FROM ${table}
    WHERE active = 1
    ORDER BY created_at DESC
`;

const query = createQuery('users', ['id', 'name', 'email']);
console.log(query);

Array.at() Method

// Array.at() for easier access to elements from the end
const arr = [1, 2, 3, 4, 5];

// Traditional way
console.log(arr[arr.length - 1]); // 5 (last element)
console.log(arr[arr.length - 2]); // 4 (second to last)

// With Array.at()
console.log(arr.at(-1)); // 5 (last element)
console.log(arr.at(-2)); // 4 (second to last)
console.log(arr.at(0));  // 1 (first element)

// Works with strings too
const str = "hello";
console.log(str.at(-1)); // "o"

Object.hasOwn()

// Object.hasOwn() - safer alternative to hasOwnProperty
const obj = {
    name: "Alice",
    age: 30
};

// Traditional way (can be problematic)
console.log(obj.hasOwnProperty("name")); // true

// What if hasOwnProperty is overridden?
obj.hasOwnProperty = () => false;
console.log(obj.hasOwnProperty("name")); // false (wrong!)

// Object.hasOwn() is safer
console.log(Object.hasOwn(obj, "name")); // true (correct)

// Works even with null prototype objects
const nullObj = Object.create(null);
nullObj.prop = "value";
// nullObj.hasOwnProperty("prop"); // TypeError!
console.log(Object.hasOwn(nullObj, "prop")); // true

Performance Considerations with Modern Features

// Optional chaining has performance cost
// ❌ Overuse can be expensive
function processUser(user) {
    return {
        name: user?.profile?.personal?.name,
        email: user?.profile?.contact?.email,
        phone: user?.profile?.contact?.phone,
        address: user?.profile?.contact?.address?.street
    };
}

// ✅ Better approach
function processUserOptimized(user) {
    const profile = user?.profile;
    if (!profile) return null;
    
    const personal = profile.personal;
    const contact = profile.contact;
    
    return {
        name: personal?.name,
        email: contact?.email,
        phone: contact?.phone,
        address: contact?.address?.street
    };
}

// Proxy performance consideration
// ❌ Proxy adds overhead
const expensiveProxy = new Proxy({}, {
    get(target, prop) {
        console.log(`Accessing ${prop}`);
        return target[prop];
    }
});

// ✅ Use Proxy judiciously for specific use cases

Next Chapter: Chapter 15: Performance and Best Practices

Previous Chapter: Chapter 13: The Interview Gauntlet - Common Tricks and Gotchas

Table of Contents: JavaScript Guide