ES2015+ Advanced Features

1. Map and Set Collections and Methods

Map vs Object

Feature Map Object
Key Types Any type (objects, functions, primitives) Strings and Symbols only
Key Order Insertion order guaranteed Insertion order (ES2015+) but complex
Size map.size property Must compute manually
Iteration Directly iterable with for...of Need Object.keys/values/entries
Performance Better for frequent add/delete Better for simple storage/lookup
Prototype No inherited keys Prototype chain pollution risk
JSON Not serializable directly JSON.stringify/parse support

Map Methods

Method Syntax Description Returns
set() map.set(key, value) Add or update key-value pair Map instance (chainable)
get() map.get(key) Retrieve value for key Value or undefined
has() map.has(key) Check if key exists Boolean
delete() map.delete(key) Remove key-value pair Boolean (true if existed)
clear() map.clear() Remove all entries undefined
keys() map.keys() Get iterator of keys MapIterator
values() map.values() Get iterator of values MapIterator
entries() map.entries() Get iterator of [key, value] pairs MapIterator
forEach() map.forEach(callback, thisArg) Execute callback for each entry undefined

Set vs Array

Feature Set Array
Uniqueness Only unique values (automatic) Can have duplicates
Lookup Speed O(1) for has() O(n) for includes()
Order Insertion order preserved Insertion order (indexed)
Index Access No index access array[index] access
Use Case Unique collections, fast lookup Ordered lists, frequent access

Set Methods

Method Syntax Description Returns
add() set.add(value) Add value to set Set instance (chainable)
has() set.has(value) Check if value exists Boolean
delete() set.delete(value) Remove value from set Boolean (true if existed)
clear() set.clear() Remove all values undefined
keys() set.keys() Get iterator of values (alias for values()) SetIterator
values() set.values() Get iterator of values SetIterator
entries() set.entries() Get iterator of [value, value] pairs SetIterator
forEach() set.forEach(callback, thisArg) Execute callback for each value undefined

Example: Map with different key types

// Create Map
const map = new Map();

// Different key types
const objKey = {id: 1};
const funcKey = () => {};
const numKey = 42;

map.set(objKey, 'object key');
map.set(funcKey, 'function key');
map.set(numKey, 'number key');
map.set('string', 'string key');

// Retrieve values
console.log(map.get(objKey));    // "object key"
console.log(map.get(funcKey));   // "function key"
console.log(map.get(42));        // "number key"

// Size and existence
console.log(map.size);           // 4
console.log(map.has(objKey));    // true
console.log(map.has({id: 1}));   // false (different object)

// Chaining
map.set('a', 1).set('b', 2).set('c', 3);

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

// Iterate keys only
for (const key of map.keys()) {
    console.log(key);
}

// Iterate values only
for (const value of map.values()) {
    console.log(value);
}

// forEach with context
map.forEach(function(value, key, map) {
    console.log(`${key} = ${value}`);
});

// Convert to array
const entries = [...map];           // [[key, val], ...]
const keys = [...map.keys()];       // [key1, key2, ...]
const values = [...map.values()];   // [val1, val2, ...]

// Delete and clear
map.delete(numKey);   // true
map.clear();          // All entries removed

Example: Set operations and uniqueness

// Create Set
const set = new Set();

// Add values
set.add(1);
set.add(2);
set.add(3);
set.add(2);  // Duplicate ignored

console.log(set.size);  // 3 (only unique values)

// Chaining
set.add(4).add(5).add(6);

// Check existence
console.log(set.has(3));  // true
console.log(set.has(10)); // false

// Remove duplicates from array
const arr = [1, 2, 3, 2, 4, 3, 5];
const unique = [...new Set(arr)];  // [1, 2, 3, 4, 5]

// Iteration
for (const value of set) {
    console.log(value);
}

set.forEach(value => {
    console.log(value);
});

// Convert to array
const array = [...set];
const array2 = Array.from(set);

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

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

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

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

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

// Delete and clear
set.delete(3);  // true
set.clear();    // All values removed

Example: Practical Map use cases

// Cache with object keys
const cache = new Map();

function expensiveOperation(obj) {
    if (cache.has(obj)) {
        return cache.get(obj);
    }
    
    const result = /* expensive computation */;
    cache.set(obj, result);
    return result;
}

// Counting occurrences
function countOccurrences(arr) {
    const counts = new Map();
    
    for (const item of arr) {
        counts.set(item, (counts.get(item) || 0) + 1);
    }
    
    return counts;
}

const items = ['a', 'b', 'a', 'c', 'b', 'a'];
const counts = countOccurrences(items);  // Map: {a => 3, b => 2, c => 1}

// Grouping by property
function groupBy(arr, key) {
    return arr.reduce((map, item) => {
        const group = item[key];
        if (!map.has(group)) {
            map.set(group, []);
        }
        map.get(group).push(item);
        return map;
    }, new Map());
}

const users = [
    {name: 'Alice', role: 'admin'},
    {name: 'Bob', role: 'user'},
    {name: 'Charlie', role: 'admin'}
];
const byRole = groupBy(users, 'role');
// Map: {admin => [{Alice}, {Charlie}], user => [{Bob}]}

// Map from array of pairs
const map2 = new Map([
    ['key1', 'value1'],
    ['key2', 'value2'],
    ['key3', 'value3']
]);

// Convert object to Map
const obj = {a: 1, b: 2, c: 3};
const map3 = new Map(Object.entries(obj));

// Convert Map to object
const mapToObj = Object.fromEntries(map3);
Performance Tips: Map is faster than Object for frequent additions/deletions. Set.has() is O(1) vs Array.includes() O(n). Use Map for non-string keys. Use Set for unique collections. Both maintain insertion order.

2. Proxy and Reflect API for Metaprogramming

Proxy Traps (Handler Methods)

Trap Intercepts Parameters Use Case
get Property access (target, prop, receiver) Validation, default values, logging
set Property assignment (target, prop, value, receiver) Validation, reactivity, computed
has in operator (target, prop) Hide properties, virtual props
deleteProperty delete operator (target, prop) Prevent deletion, logging
apply Function call (target, thisArg, args) Logging, parameter validation
construct new operator (target, args, newTarget) Factory pattern, validation
getPrototypeOf Object.getPrototypeOf() (target) Custom prototype behavior
setPrototypeOf Object.setPrototypeOf() (target, proto) Prevent prototype changes
isExtensible Object.isExtensible() (target) Custom extensibility logic
preventExtensions Object.preventExtensions() (target) Custom prevention logic
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() (target, prop) Hide/modify descriptors
defineProperty Object.defineProperty() (target, prop, descriptor) Intercept property definitions
ownKeys Object.keys(), for...in (target) Filter properties, add virtual

Reflect API Methods

Method Description Returns
Reflect.get() Get property value Property value
Reflect.set() Set property value Boolean (success)
Reflect.has() Check if property exists Boolean
Reflect.deleteProperty() Delete property Boolean (success)
Reflect.apply() Call function with args Function result
Reflect.construct() Call constructor with new New instance
Reflect.getPrototypeOf() Get prototype Prototype object or null
Reflect.setPrototypeOf() Set prototype Boolean (success)
Reflect.isExtensible() Check if extensible Boolean
Reflect.preventExtensions() Make non-extensible Boolean (success)
Reflect.getOwnPropertyDescriptor() Get property descriptor Descriptor object or undefined
Reflect.defineProperty() Define property Boolean (success)
Reflect.ownKeys() Get all own property keys Array of keys

Example: Basic Proxy with get and set traps

// Simple property access logging
const target = {
    name: 'Alice',
    age: 30
};

const handler = {
    get(target, prop, receiver) {
        console.log(`Getting ${prop}`);
        return Reflect.get(target, prop, receiver);
    },
    
    set(target, prop, value, receiver) {
        console.log(`Setting ${prop} to ${value}`);
        return Reflect.set(target, prop, value, receiver);
    }
};

const proxy = new Proxy(target, handler);

proxy.name;        // Logs: "Getting name", returns "Alice"
proxy.age = 31;    // Logs: "Setting age to 31"

// Validation proxy
const validatedUser = new Proxy({}, {
    set(target, prop, value) {
        if (prop === 'age') {
            if (!Number.isInteger(value) || value < 0) {
                throw new TypeError('Age must be a non-negative integer');
            }
        }
        
        if (prop === 'email') {
            if (!value.includes('@')) {
                throw new TypeError('Invalid email format');
            }
        }
        
        return Reflect.set(target, prop, value);
    }
});

validatedUser.age = 25;           // OK
validatedUser.email = 'a@b.com';  // OK
// validatedUser.age = -5;        // TypeError
// validatedUser.age = 'old';     // TypeError
// validatedUser.email = 'bad';   // TypeError

Example: Default values and negative indexing

// Default values for undefined properties
const withDefaults = new Proxy({}, {
    get(target, prop) {
        return prop in target ? target[prop] : 'default value';
    }
});

console.log(withDefaults.name);     // "default value"
withDefaults.name = 'Alice';
console.log(withDefaults.name);     // "Alice"

// Negative array indexing (Python-style)
function createArray(arr) {
    return new Proxy(arr, {
        get(target, prop) {
            const index = Number(prop);
            
            // Handle negative indices
            if (index < 0) {
                return target[target.length + index];
            }
            
            return Reflect.get(target, prop);
        }
    });
}

const arr = createArray(['a', 'b', 'c', 'd', 'e']);
console.log(arr[-1]);  // 'e'
console.log(arr[-2]);  // 'd'
console.log(arr[0]);   // 'a'

// Read-only object
function createReadOnly(obj) {
    return new Proxy(obj, {
        set() {
            throw new Error('Object is read-only');
        },
        deleteProperty() {
            throw new Error('Object is read-only');
        }
    });
}

const readOnly = createReadOnly({name: 'Alice', age: 30});
console.log(readOnly.name);  // "Alice"
// readOnly.name = 'Bob';    // Error: Object is read-only
// delete readOnly.age;      // Error: Object is read-only

Example: Function call interception

// Log all function calls
function createLoggingProxy(func, name) {
    return new Proxy(func, {
        apply(target, thisArg, args) {
            console.log(`Calling ${name} with args:`, args);
            const result = Reflect.apply(target, thisArg, args);
            console.log(`${name} returned:`, result);
            return result;
        }
    });
}

function add(a, b) {
    return a + b;
}

const loggedAdd = createLoggingProxy(add, 'add');
loggedAdd(5, 3);
// Logs: "Calling add with args: [5, 3]"
// Logs: "add returned: 8"

// Memoization proxy
function createMemoized(func) {
    const cache = new Map();
    
    return new Proxy(func, {
        apply(target, thisArg, args) {
            const key = JSON.stringify(args);
            
            if (cache.has(key)) {
                console.log('Cache hit for', args);
                return cache.get(key);
            }
            
            console.log('Computing for', args);
            const result = Reflect.apply(target, thisArg, args);
            cache.set(key, result);
            return result;
        }
    });
}

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoFib = createMemoized(fibonacci);
console.log(memoFib(5));  // Computes
console.log(memoFib(5));  // Cache hit

// Constructor interception
class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

const UserProxy = new Proxy(User, {
    construct(target, args) {
        console.log('Creating user with:', args);
        
        // Add timestamp to all instances
        const instance = Reflect.construct(target, args);
        instance.createdAt = Date.now();
        return instance;
    }
});

const user = new UserProxy('Alice', 30);
console.log(user.createdAt);  // timestamp added automatically

Example: Observable/reactive object

// Reactive object that triggers callbacks on changes
function createObservable(target, callback) {
    return new Proxy(target, {
        set(target, prop, value, receiver) {
            const oldValue = target[prop];
            const result = Reflect.set(target, prop, value, receiver);
            
            if (oldValue !== value) {
                callback(prop, oldValue, value);
            }
            
            return result;
        },
        
        deleteProperty(target, prop) {
            const oldValue = target[prop];
            const result = Reflect.deleteProperty(target, prop);
            
            if (result) {
                callback(prop, oldValue, undefined);
            }
            
            return result;
        }
    });
}

const state = createObservable(
    {count: 0, name: 'Alice'},
    (prop, oldVal, newVal) => {
        console.log(`${prop} changed from ${oldVal} to ${newVal}`);
    }
);

state.count = 5;      // Logs: "count changed from 0 to 5"
state.name = 'Bob';   // Logs: "name changed from Alice to Bob"
delete state.count;   // Logs: "count changed from 5 to undefined"

// Virtual properties
const user2 = new Proxy({
    firstName: 'John',
    lastName: 'Doe'
}, {
    get(target, prop) {
        if (prop === 'fullName') {
            return `${target.firstName} ${target.lastName}`;
        }
        return Reflect.get(target, prop);
    },
    
    set(target, prop, value) {
        if (prop === 'fullName') {
            const [first, last] = value.split(' ');
            target.firstName = first;
            target.lastName = last;
            return true;
        }
        return Reflect.set(target, prop, value);
    }
});

console.log(user2.fullName);      // "John Doe"
user2.fullName = 'Jane Smith';
console.log(user2.firstName);     // "Jane"
console.log(user2.lastName);      // "Smith"
Important: Always use Reflect methods in Proxy handlers for proper behavior and receiver context. Proxy traps must respect invariants (e.g., can't report non-existent property as non-configurable). Proxies have performance overhead - use judiciously. Revocable proxies: Proxy.revocable(target, handler) returns {proxy, revoke()}.

3. Generator Functions and Iterator Protocol

Generator Function Syntax

Type Syntax Example
Function Declaration function* name() {} function* gen() { yield 1; }
Function Expression const f = function*() {} const g = function*() { yield 1; }
Object Method obj = { *method() {} } { *gen() { yield 1; } }
Class Method class C { *method() {} } class C { *gen() { yield 1; } }

Generator Methods

Method Syntax Description Returns
next() gen.next(value) Resume execution, send value to yield {value, done}
return() gen.return(value) Terminate generator, return value {value, done: true}
throw() gen.throw(error) Throw error at yield point {value, done} or throws

yield Expressions

Expression Syntax Description
yield yield value Pause and return value
yield* yield* iterable Delegate to another generator/iterable
yield (receive) const x = yield Receive value from next(value)

Example: Basic generator function

// Simple generator
function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = numberGenerator();

console.log(gen.next());  // {value: 1, done: false}
console.log(gen.next());  // {value: 2, done: false}
console.log(gen.next());  // {value: 3, done: false}
console.log(gen.next());  // {value: undefined, done: true}

// Generators are iterable
for (const num of numberGenerator()) {
    console.log(num);  // 1, 2, 3
}

// Spread with generator
const numbers = [...numberGenerator()];  // [1, 2, 3]

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

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

// Fibonacci generator
function* fibonacci() {
    let [a, b] = [0, 1];
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

const fib = fibonacci();
console.log(fib.next().value);  // 0
console.log(fib.next().value);  // 1
console.log(fib.next().value);  // 1
console.log(fib.next().value);  // 2
console.log(fib.next().value);  // 3
console.log(fib.next().value);  // 5

Example: Two-way communication with generators

// Generator that receives values
function* twoWayGenerator() {
    console.log('Generator started');
    
    const a = yield 'First yield';
    console.log('Received:', a);
    
    const b = yield 'Second yield';
    console.log('Received:', b);
    
    return 'Done';
}

const gen2 = twoWayGenerator();

console.log(gen2.next());        // {value: "First yield", done: false}
                                 // Logs: "Generator started"

console.log(gen2.next(10));      // {value: "Second yield", done: false}
                                 // Logs: "Received: 10"

console.log(gen2.next(20));      // {value: "Done", done: true}
                                 // Logs: "Received: 20"

// ID generator with reset
function* idGenerator() {
    let id = 1;
    
    while (true) {
        const reset = yield id++;
        if (reset) {
            id = 1;
        }
    }
}

const ids = idGenerator();
console.log(ids.next().value);       // 1
console.log(ids.next().value);       // 2
console.log(ids.next().value);       // 3
console.log(ids.next(true).value);   // 1 (reset)
console.log(ids.next().value);       // 2

Example: yield* delegation

// Delegate to another generator
function* gen1() {
    yield 1;
    yield 2;
}

function* gen2() {
    yield 'a';
    yield* gen1();  // Delegate
    yield 'b';
}

console.log([...gen2()]);  // ['a', 1, 2, 'b']

// Delegate to iterable
function* gen3() {
    yield* [1, 2, 3];
    yield* 'hello';
}

console.log([...gen3()]);  // [1, 2, 3, 'h', 'e', 'l', 'l', 'o']

// Tree traversal with delegation
const tree = {
    value: 1,
    children: [
        {
            value: 2,
            children: [
                { value: 4, children: [] },
                { value: 5, children: [] }
            ]
        },
        {
            value: 3,
            children: [
                { value: 6, children: [] }
            ]
        }
    ]
};

function* traverse(node) {
    yield node.value;
    for (const child of node.children) {
        yield* traverse(child);
    }
}

console.log([...traverse(tree)]);  // [1, 2, 4, 5, 3, 6]

// Flatten nested arrays
function* flatten(arr) {
    for (const item of arr) {
        if (Array.isArray(item)) {
            yield* flatten(item);
        } else {
            yield item;
        }
    }
}

const nested = [1, [2, [3, 4], 5], 6];
console.log([...flatten(nested)]);  // [1, 2, 3, 4, 5, 6]

Example: Generator control flow

// Early termination with return()
function* gen4() {
    try {
        yield 1;
        yield 2;
        yield 3;
    } finally {
        console.log('Cleanup');
    }
}

const g4 = gen4();
console.log(g4.next());      // {value: 1, done: false}
console.log(g4.return(99));  // Logs: "Cleanup"
                             // {value: 99, done: true}
console.log(g4.next());      // {value: undefined, done: true}

// Error handling with throw()
function* gen5() {
    try {
        yield 1;
        yield 2;
        yield 3;
    } catch (e) {
        console.log('Caught:', e.message);
        yield 'error handled';
    }
}

const g5 = gen5();
console.log(g5.next());                     // {value: 1, done: false}
console.log(g5.throw(new Error('Oops')));   // Logs: "Caught: Oops"
                                            // {value: "error handled", done: false}
console.log(g5.next());                     // {value: 3, done: false}

// Async-like flow control (pre-async/await pattern)
function run(genFunc) {
    const gen = genFunc();
    
    function handle(result) {
        if (result.done) return Promise.resolve(result.value);
        
        return Promise.resolve(result.value)
            .then(res => handle(gen.next(res)))
            .catch(err => handle(gen.throw(err)));
    }
    
    return handle(gen.next());
}

function* fetchUser() {
    try {
        const response = yield fetch('/api/user');
        const data = yield response.json();
        console.log(data);
    } catch (e) {
        console.error('Error:', e);
    }
}

// run(fetchUser);
Use Cases: Generators excel at: lazy evaluation (generate values on demand), infinite sequences, state machines, async control flow (pre-async/await), tree traversal, streaming data. Generators are pausable/resumable functions that return iterators.

4. Module System (import/export, dynamic imports)

Export Syntax

Type Syntax Description
Named Export export const x = 1; Export named binding
Named Export (list) export {x, y, z}; Export multiple named bindings
Named Export (rename) export {x as myX}; Export with different name
Default Export export default value; Export default value (one per module)
Default + Named export {x, y, z as default}; Mix default and named exports
Re-export export {x} from './mod'; Re-export from another module
Re-export All export * from './mod'; Re-export all named exports
Re-export All (namespace) export * as ns from './mod'; Re-export as namespace object

Import Syntax

Type Syntax Description
Named Import import {x, y} from './mod'; Import specific named exports
Named Import (rename) import {x as myX} from './mod'; Import with different name
Default Import import x from './mod'; Import default export
Default + Named import x, {y, z} from './mod'; Import default and named together
Namespace Import import * as mod from './mod'; Import all as namespace object
Side Effect Only import './mod'; Execute module (no imports)
Dynamic Import import('./mod').then(...) Load module asynchronously at runtime

Module Features

Feature Description Behavior
Strict Mode Always in strict mode No need for 'use strict'
Top-level this undefined (not global) Different from scripts
Hoisting Imports hoisted to top Executed before module code
Live Bindings Imports are references Updates reflect in importers
Singleton Module evaluated once Cached for all imports
Scope Module scope (not global) Variables are private by default
Static Structure Imports/exports must be top-level Analyzed before execution

Example: Named exports and imports

// math.js - Named exports
export const PI = 3.14159;
export const E = 2.71828;

export function add(a, b) {
    return a + b;
}

export function multiply(a, b) {
    return a * b;
}

// Alternative: Export list
const PI2 = 3.14159;
const E2 = 2.71828;

function add2(a, b) {
    return a + b;
}

function multiply2(a, b) {
    return a * b;
}

export { PI2, E2, add2, multiply2 };

// Export with rename
export { add2 as addition, multiply2 as multiplication };

// ===== Importing =====

// app.js - Import specific named exports
import { PI, add, multiply } from './math.js';

console.log(PI);           // 3.14159
console.log(add(2, 3));    // 5

// Import with rename
import { add as sum, multiply as product } from './math.js';

console.log(sum(2, 3));       // 5
console.log(product(2, 3));   // 6

// Import all as namespace
import * as math from './math.js';

console.log(math.PI);           // 3.14159
console.log(math.add(2, 3));    // 5

// Import multiple
import { PI, E, add, multiply } from './math.js';

Example: Default exports

// user.js - Default export (class)
export default class User {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        return `Hello, ${this.name}`;
    }
}

// calculator.js - Default export (function)
export default function calculate(a, b, op) {
    switch (op) {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/': return a / b;
        default: return NaN;
    }
}

// config.js - Default export (object)
export default {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
};

// ===== Importing =====

// Import default (can use any name)
import User from './user.js';
import calculate from './calculator.js';
import config from './config.js';

const user = new User('Alice');
console.log(user.greet());          // "Hello, Alice"

console.log(calculate(5, 3, '+'));  // 8
console.log(config.apiUrl);         // "https://api.example.com"

// Mix default and named exports
// utils.js
export default function log(msg) {
    console.log(msg);
}

export const VERSION = '1.0.0';
export const DEBUG = true;

// app.js
import log, { VERSION, DEBUG } from './utils.js';

log('Starting app');
console.log(VERSION);  // "1.0.0"

Example: Re-exports and barrel exports

// components/Button.js
export default class Button {}

// components/Input.js
export default class Input {}

// components/Form.js
export default class Form {}

// components/index.js - Barrel export
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Form } from './Form.js';

// Or re-export everything
export * from './Button.js';
export * from './Input.js';
export * from './Form.js';

// ===== Usage =====

// app.js - Single import for all components
import { Button, Input, Form } from './components/index.js';

// Re-export with namespace
// components/index.js
export * as Button from './Button.js';
export * as Input from './Input.js';

// app.js
import { Button, Input } from './components/index.js';

Button.default;  // Access default export
Button.someOtherExport;  // Access named exports

// Aggregate re-exports
// api/index.js
export * from './users.js';      // Re-export all from users
export * from './products.js';   // Re-export all from products
export * from './orders.js';     // Re-export all from orders

// Now import all API functions from one place
import { getUser, getProduct, getOrder } from './api/index.js';

Example: Dynamic imports

// Dynamic import returns a Promise
async function loadModule() {
    const module = await import('./math.js');
    
    console.log(module.PI);           // 3.14159
    console.log(module.add(2, 3));    // 5
}

loadModule();

// Conditional loading
async function loadComponent(name) {
    let module;
    
    if (name === 'dashboard') {
        module = await import('./Dashboard.js');
    } else if (name === 'settings') {
        module = await import('./Settings.js');
    }
    
    return module.default;
}

// Lazy loading with error handling
async function lazyLoad() {
    try {
        const { someFunction } = await import('./heavy-module.js');
        someFunction();
    } catch (error) {
        console.error('Failed to load module:', error);
    }
}

// Code splitting in routes
const routes = {
    '/home': () => import('./pages/Home.js'),
    '/about': () => import('./pages/About.js'),
    '/contact': () => import('./pages/Contact.js')
};

async function navigate(path) {
    const loader = routes[path];
    if (loader) {
        const module = await loader();
        module.default.render();
    }
}

// Dynamic import with destructuring
button.addEventListener('click', async () => {
    const { calculate, PI } = await import('./math.js');
    console.log(calculate(PI, 2, '*'));
});

// Load multiple modules in parallel
async function loadMultiple() {
    const [module1, module2, module3] = await Promise.all([
        import('./module1.js'),
        import('./module2.js'),
        import('./module3.js')
    ]);
    
    // Use modules...
}

// Feature detection with dynamic import
if ('IntersectionObserver' in window) {
    // Native support
} else {
    // Load polyfill
    await import('./intersection-observer-polyfill.js');
}
Key Points: Modules are singletons (evaluated once, cached). Imports are live bindings (not copies). Static imports are hoisted and synchronous. Dynamic imports are asynchronous and enable code splitting. Use type="module" in script tags. Modules always in strict mode with private scope.

5. Private Fields and Class Features

Private Field Syntax

Feature Syntax Description Support
Private Field #fieldName Private instance field (truly private) ES2022
Private Method #methodName() {} Private instance method ES2022
Private Static Field static #field Private static field ES2022
Private Static Method static #method() {} Private static method ES2022
Private Getter get #prop() {} Private getter accessor ES2022
Private Setter set #prop(val) {} Private setter accessor ES2022

Class Field Features

Feature Syntax Description Support
Public Field fieldName = value; Public instance field with initializer ES2022
Static Field static field = value; Static field (on class itself) ES2022
Static Block static { /* code */ } Static initialization block ES2022
Private in #field in obj Check if object has private field ES2022

Example: Private fields and methods

class BankAccount {
    // Private fields (must be declared)
    #balance = 0;
    #accountNumber;
    #transactions = [];
    
    // Public field
    accountType = 'checking';
    
    constructor(accountNumber, initialBalance) {
        this.#accountNumber = accountNumber;
        this.#balance = initialBalance;
        this.#addTransaction('Initial deposit', initialBalance);
    }
    
    // Private method
    #addTransaction(type, amount) {
        this.#transactions.push({
            type,
            amount,
            date: new Date(),
            balance: this.#balance
        });
    }
    
    // Public methods accessing private fields
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            this.#addTransaction('Deposit', amount);
            return true;
        }
        return false;
    }
    
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            this.#addTransaction('Withdrawal', -amount);
            return true;
        }
        return false;
    }
    
    getBalance() {
        return this.#balance;
    }
    
    getAccountNumber() {
        // Return masked number
        return '****' + this.#accountNumber.slice(-4);
    }
    
    getTransactions() {
        // Return copy to prevent modification
        return [...this.#transactions];
    }
}

const account = new BankAccount('1234567890', 1000);

console.log(account.getBalance());        // 1000
account.deposit(500);                     // true
console.log(account.getBalance());        // 1500

// Private fields truly inaccessible from outside
console.log(account.#balance);            // SyntaxError
console.log(account.balance);             // undefined
console.log(account['#balance']);         // undefined

// Check if object has private field (from inside class only)
class Demo {
    #private = 42;
    
    static hasPrivate(obj) {
        return #private in obj;
    }
}

const demo = new Demo();
console.log(Demo.hasPrivate(demo));       // true
console.log(Demo.hasPrivate({}));         // false

Example: Private getters and setters

class User {
    #firstName;
    #lastName;
    #age;
    
    constructor(firstName, lastName, age) {
        this.#firstName = firstName;
        this.#lastName = lastName;
        this.#age = age;
    }
    
    // Private getter
    get #fullName() {
        return `${this.#firstName} ${this.#lastName}`;
    }
    
    // Private setter with validation
    set #age(value) {
        if (value < 0 || value > 150) {
            throw new Error('Invalid age');
        }
        this.#age = value;
    }
    
    // Public method using private getter
    introduce() {
        return `Hi, I'm ${this.#fullName}, ${this.#age} years old`;
    }
    
    // Public method using private setter
    celebrateBirthday() {
        this.#age = this.#age + 1;
    }
    
    getAge() {
        return this.#age;
    }
}

const user = new User('John', 'Doe', 30);
console.log(user.introduce());  // "Hi, I'm John Doe, 30 years old"
user.celebrateBirthday();
console.log(user.getAge());     // 31

Example: Static private fields and methods

class DatabaseConnection {
    // Private static field for singleton instance
    static #instance = null;
    static #connectionCount = 0;
    
    // Private instance field
    #connectionId;
    
    constructor() {
        if (DatabaseConnection.#instance) {
            throw new Error('Use DatabaseConnection.getInstance()');
        }
        
        this.#connectionId = ++DatabaseConnection.#connectionCount;
        DatabaseConnection.#instance = this;
    }
    
    // Private static method
    static #validateConfig(config) {
        if (!config.host || !config.database) {
            throw new Error('Invalid configuration');
        }
    }
    
    // Public static method (singleton pattern)
    static getInstance(config) {
        if (!DatabaseConnection.#instance) {
            DatabaseConnection.#validateConfig(config);
            DatabaseConnection.#instance = new DatabaseConnection();
        }
        return DatabaseConnection.#instance;
    }
    
    // Static method to get connection count
    static getConnectionCount() {
        return DatabaseConnection.#connectionCount;
    }
    
    getConnectionId() {
        return this.#connectionId;
    }
}

const db1 = DatabaseConnection.getInstance({host: 'localhost', database: 'test'});
const db2 = DatabaseConnection.getInstance({});

console.log(db1 === db2);                           // true (singleton)
console.log(DatabaseConnection.getConnectionCount());  // 1
console.log(db1.getConnectionId());                 // 1

// Can't access private static field
console.log(DatabaseConnection.#instance);          // SyntaxError

Example: Static initialization blocks

class Config {
    static apiUrl;
    static timeout;
    static headers;
    
    // Static initialization block
    static {
        // Complex initialization logic
        const env = process.env.NODE_ENV || 'development';
        
        if (env === 'production') {
            this.apiUrl = 'https://api.prod.example.com';
            this.timeout = 5000;
        } else {
            this.apiUrl = 'https://api.dev.example.com';
            this.timeout = 10000;
        }
        
        this.headers = {
            'Content-Type': 'application/json',
            'X-API-Version': '1.0'
        };
        
        console.log(`Config initialized for ${env}`);
    }
    
    // Multiple static blocks are allowed
    static {
        // Additional initialization
        this.initialized = true;
    }
}

console.log(Config.apiUrl);      // URL based on environment
console.log(Config.initialized);  // true

// Private static with static block
class Counter {
    static #count = 0;
    static #instances = [];
    
    static {
        // Initialize private static fields
        console.log('Counter class initialized');
    }
    
    constructor(name) {
        this.name = name;
        Counter.#count++;
        Counter.#instances.push(this);
    }
    
    static getCount() {
        return Counter.#count;
    }
    
    static getInstances() {
        return [...Counter.#instances];
    }
}

const c1 = new Counter('First');
const c2 = new Counter('Second');
console.log(Counter.getCount());  // 2
Private Fields vs WeakMap: Private fields (#) are truly private (not accessible via reflection/bracket notation). WeakMap pattern was used before but less ergonomic. Private fields require declaration. Use #field in obj to check existence. Static blocks enable complex static initialization.

6. Optional Chaining and Nullish Coalescing

Optional Chaining Operators

Operator Syntax Description Returns
Property Access obj?.prop Access property if obj not null/undefined Value or undefined
Deep Property obj?.prop?.nested Chain multiple optional accesses Value or undefined
Bracket Notation obj?.[expr] Optional computed property access Value or undefined
Function Call func?.(args) Call function if it exists Value or undefined
Method Call obj.method?.(args) Call method if it exists Value or undefined

Nullish Coalescing vs OR

Operator Syntax Falsy Values Use When
Logical OR a || b Returns b for: false, 0, '', null, undefined, NaN Want any falsy to use default
Nullish Coalescing a ?? b Returns b only for: null, undefined Want to preserve 0, false, ''

Comparison Table

Value value || 'default' value ?? 'default'
null 'default' 'default'
undefined 'default' 'default'
false 'default' false
0 'default' 0
'' 'default' ''
NaN 'default' NaN
'hello' 'hello' 'hello'

Example: Optional chaining basics

// Without optional chaining (verbose)
let name1;
if (user && user.profile && user.profile.name) {
    name1 = user.profile.name;
}

// With optional chaining (concise)
const name2 = user?.profile?.name;

// Practical examples
const user = {
    name: 'Alice',
    address: {
        street: '123 Main St',
        city: 'Boston'
    }
};

// Safe property access
console.log(user?.name);              // "Alice"
console.log(user?.email);             // undefined
console.log(user?.address?.city);     // "Boston"
console.log(user?.address?.zip);      // undefined

// Works with null/undefined
const nullUser = null;
console.log(nullUser?.name);          // undefined (no error)

const undefinedUser = undefined;
console.log(undefinedUser?.name);     // undefined (no error)

// Array access
const users = [
    {name: 'Alice'},
    null,
    {name: 'Bob'}
];

console.log(users[0]?.name);          // "Alice"
console.log(users[1]?.name);          // undefined (no error)
console.log(users[2]?.name);          // "Bob"
console.log(users[10]?.name);         // undefined

// Computed property access
const key = 'address';
console.log(user?.[key]?.city);       // "Boston"

const dynamicKey = null;
console.log(user?.[dynamicKey]);      // undefined

Example: Optional chaining with function calls

// Optional function call
const obj = {
    method() {
        return 'called';
    }
};

console.log(obj.method?.());          // "called"
console.log(obj.missing?.());         // undefined (no error)

// Optional method call on potentially null object
let calculator = null;

console.log(calculator?.add?.(2, 3)); // undefined (no error)

calculator = {
    add(a, b) {
        return a + b;
    }
};

console.log(calculator?.add?.(2, 3)); // 5

// Callback handling
function processData(data, callback) {
    const result = /* process data */;
    callback?.(result);  // Only call if callback exists
}

processData([1, 2, 3]);                    // No error if no callback
processData([1, 2, 3], res => console.log(res));  // Callback called

// Event handlers
button.addEventListener('click', (e) => {
    e.target?.classList?.toggle?.('active');
});

// API calls with optional error handler
async function fetchData(url, options) {
    try {
        const response = await fetch(url);
        const data = await response.json();
        options?.onSuccess?.(data);
        return data;
    } catch (error) {
        options?.onError?.(error);
        throw error;
    }
}

Example: Nullish coalescing operator

// Basic usage
const value1 = null ?? 'default';       // "default"
const value2 = undefined ?? 'default';  // "default"
const value3 = 'hello' ?? 'default';    // "hello"

// Preserves falsy values (unlike ||)
const count = 0 ?? 10;           // 0 (not 10)
const isActive = false ?? true;  // false (not true)
const text = '' ?? 'default';    // '' (not "default")

// Compare with ||
console.log(0 || 10);            // 10
console.log(0 ?? 10);            // 0

console.log(false || true);      // true
console.log(false ?? true);      // false

console.log('' || 'default');    // "default"
console.log('' ?? 'default');    // ''

// Use case: Configuration defaults
function createConfig(options) {
    return {
        // ?? preserves explicit 0, false, ''
        timeout: options?.timeout ?? 5000,
        retries: options?.retries ?? 3,
        debug: options?.debug ?? false,
        prefix: options?.prefix ?? '',
        
        // Bad: || would replace 0, false, ''
        // timeout: options?.timeout || 5000,  // 0 becomes 5000!
    };
}

console.log(createConfig({timeout: 0}));  // {timeout: 0, ...}
console.log(createConfig({}));            // {timeout: 5000, ...}

// Chaining
const result = a ?? b ?? c ?? 'default';

// Cannot mix with && or || without parentheses
// const x = a ?? b || c;     // SyntaxError
const x = (a ?? b) || c;      // OK
const y = a ?? (b || c);      // OK

Example: Combined optional chaining and nullish coalescing

// Powerful combination
const config = {
    api: {
        timeout: 0,
        retries: 3
    }
};

// Get nested value with default
const timeout = config?.api?.timeout ?? 5000;  // 0 (preserved)
const maxRetries = config?.api?.retries ?? 3;  // 3
const debug = config?.api?.debug ?? false;     // false (default)

// User preferences with defaults
function getUserPreference(user, key, defaultValue) {
    return user?.preferences?.[key] ?? defaultValue;
}

const user = {
    preferences: {
        theme: 'dark',
        fontSize: 14,
        notifications: false
    }
};

console.log(getUserPreference(user, 'theme', 'light'));          // "dark"
console.log(getUserPreference(user, 'fontSize', 16));            // 14
console.log(getUserPreference(user, 'notifications', true));     // false
console.log(getUserPreference(user, 'language', 'en'));          // "en"
console.log(getUserPreference(null, 'theme', 'light'));          // "light"

// Real-world: Form field values
function getFieldValue(formData, fieldName, defaultValue = '') {
    return formData?.fields?.[fieldName]?.value ?? defaultValue;
}

// Real-world: API response handling
async function fetchUser(userId) {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    
    return {
        id: data?.id ?? userId,
        name: data?.name ?? 'Unknown',
        email: data?.email ?? 'no-email@example.com',
        avatar: data?.profile?.avatar?.url ?? '/default-avatar.png',
        role: data?.permissions?.role ?? 'user',
        settings: {
            theme: data?.settings?.theme ?? 'light',
            language: data?.settings?.language ?? 'en',
            notifications: data?.settings?.notifications ?? true
        }
    };
}

// Optional call with default result
const result2 = obj.method?.() ?? 'no result';

// Chain optional property with default
const city = user?.address?.city ?? 'Unknown city';
Best Practices: Use ?? when you want to preserve falsy values like 0, false, ''. Use || when any falsy should trigger default. Optional chaining ?. short-circuits (stops evaluating if null/undefined). Both operators improve code readability and reduce defensive checks. Cannot mix ?? with && or || without parentheses.

7. Logical Assignment and Numeric Separators

Logical Assignment Operators

Operator Syntax Equivalent To Assigns When
Logical OR Assignment x ||= y x || (x = y) x is falsy
Logical AND Assignment x &&= y x && (x = y) x is truthy
Nullish Assignment x ??= y x ?? (x = y) x is null or undefined

Behavior Comparison

Initial Value x ||= 10 x &&= 10 x ??= 10
null 10 null 10
undefined 10 undefined 10
false 10 false false
0 10 0 0
'' 10 '' ''
5 5 10 5
'hello' 'hello' 10 'hello'
true true 10 true

Numeric Separators

Type Without Separator With Separator Value
Decimal 1000000 1_000_000 1000000
Binary 0b11111111 0b1111_1111 255
Octal 0o777 0o7_7_7 511
Hexadecimal 0xFFFFFF 0xFF_FF_FF 16777215
BigInt 1000000000000n 1_000_000_000_000n 1000000000000n
Decimal 3.14159265 3.141_592_65 3.14159265
Scientific 1e10 1_000e7 10000000000

Example: Logical OR assignment (||=)

// ||= assigns only if left side is falsy

// Initialize if not set
let config = {};
config.timeout ||= 5000;        // Assigns 5000
config.timeout ||= 3000;        // No assignment (already truthy)
console.log(config.timeout);    // 5000

// Default function parameters (alternative pattern)
function greet(options) {
    options ||= {};
    options.name ||= 'Guest';
    options.greeting ||= 'Hello';
    
    return `${options.greeting}, ${options.name}!`;
}

console.log(greet());                           // "Hello, Guest!"
console.log(greet({name: 'Alice'}));           // "Hello, Alice!"
console.log(greet({greeting: 'Hi'}));          // "Hi, Guest!"

// Lazy initialization
class LazyService {
    #cache;
    
    getData() {
        // Initialize cache on first access
        this.#cache ||= this.#loadData();
        return this.#cache;
    }
    
    #loadData() {
        console.log('Loading data...');
        return {loaded: true};
    }
}

const service = new LazyService();
service.getData();  // Logs: "Loading data..."
service.getData();  // No log (cache hit)

// Array default
let items = null;
items ||= [];           // Assigns []
items.push(1, 2, 3);

// Problem: replaces falsy values you want to keep
let count = 0;
count ||= 10;           // Becomes 10 (probably not intended)
console.log(count);     // 10

// Better: use ??= for this case

Example: Logical AND assignment (&&=)

// &&= assigns only if left side is truthy

// Conditional update
let user = {name: 'Alice', admin: true};

// Only update if user is admin
user.admin &&= 'super-admin';
console.log(user.admin);  // "super-admin"

let guest = {name: 'Bob', admin: false};
guest.admin &&= 'super-admin';
console.log(guest.admin);  // false (no assignment)

// Transform if exists
let settings = {
    theme: 'dark',
    language: 'en'
};

// Uppercase language if it exists
settings.language &&= settings.language.toUpperCase();
console.log(settings.language);  // "EN"

settings.missing &&= 'value';
console.log(settings.missing);   // undefined (no assignment)

// Validation chain
function processData(data) {
    // Only proceed if data exists and is valid
    data &&= validateData(data);
    data &&= transformData(data);
    data &&= enrichData(data);
    
    return data;
}

// Increment if enabled
let feature = {
    enabled: true,
    count: 5
};

feature.enabled &&= feature.count++;
console.log(feature);  // {enabled: 5, count: 6}

// Disable feature
feature.enabled = false;
feature.enabled &&= feature.count++;
console.log(feature);  // {enabled: false, count: 6} (no increment)

Example: Nullish assignment (??=)

// ??= assigns only if left side is null or undefined
// Preserves falsy values like 0, false, ''

// Configuration with defaults (preserves explicit falsy)
const config2 = {
    timeout: 0,          // Explicit 0
    retries: undefined,  // Not set
    debug: false,        // Explicit false
    prefix: ''           // Explicit empty string
};

config2.timeout ??= 5000;   // No assignment (0 is not null/undefined)
config2.retries ??= 3;      // Assigns 3
config2.debug ??= true;     // No assignment (false is not null/undefined)
config2.prefix ??= 'api_';  // No assignment ('' is not null/undefined)
config2.cache ??= true;     // Assigns true (cache was undefined)

console.log(config2);
// {
//   timeout: 0,
//   retries: 3,
//   debug: false,
//   prefix: '',
//   cache: true
// }

// Compare with ||=
let a = 0;
let b = 0;

a ||= 10;   // a becomes 10 (0 is falsy)
b ??= 10;   // b stays 0 (0 is not null/undefined)

console.log(a, b);  // 10, 0

// Form field defaults
function setDefaults(formData) {
    formData.name ??= 'Anonymous';
    formData.age ??= 0;           // Preserve explicit 0
    formData.agree ??= false;     // Preserve explicit false
    formData.email ??= '';        // Preserve explicit empty
}

// Nested object initialization
const state = {};
state.user ??= {};
state.user.preferences ??= {};
state.user.preferences.theme ??= 'light';

// Lazy property initialization
class Resource {
    #data;
    
    get data() {
        this.#data ??= this.#load();
        return this.#data;
    }
    
    #load() {
        return {loaded: true};
    }
}

// Array element default
const arr2 = [1, null, 3, undefined, 5];
arr2[1] ??= 2;  // Assigns 2
arr2[3] ??= 4;  // Assigns 4
console.log(arr2);  // [1, 2, 3, 4, 5]

Example: Numeric separators

// Improve readability of large numbers

// Large integers
const million = 1_000_000;
const billion = 1_000_000_000;
const trillion = 1_000_000_000_000;

console.log(million);    // 1000000
console.log(billion);    // 1000000000

// Financial calculations
const price = 1_299.99;
const salary = 75_000;
const budget = 2_500_000;

// Binary (bytes)
const byte = 0b1111_1111;           // 255
const kilobyte = 0b0100_0000_0000;  // 1024

// Hexadecimal (colors)
const white = 0xFF_FF_FF;
const black = 0x00_00_00;
const red = 0xFF_00_00;
const green = 0x00_FF_00;
const blue = 0x00_00_FF;

// Scientific notation
const lightSpeed = 299_792_458;         // m/s
const plancksConstant = 6.626_070_15e-34;  // J⋅s
const avogadro = 6.022_140_76e23;       // mol⁻¹

// BigInt
const veryLarge = 9_007_199_254_740_991n;  // Max safe integer
const hugeNumber = 1_000_000_000_000_000_000_000n;

// Credit card number (for display/parsing)
const cardNumber = '4532_1234_5678_9010';  // String format

// File sizes
const KB = 1_024;
const MB = 1_024 * KB;
const GB = 1_024 * MB;
const TB = 1_024 * GB;

console.log(`1 MB = ${MB} bytes`);  // 1 MB = 1048576 bytes

// Group by thousands (US style)
const population = 328_239_523;

// Group by ten-thousands (China/Japan style)
const population2 = 3_2823_9523;

// Custom grouping for phone numbers (display only as string)
// const phone = 555_123_4567;  // Numbers don't include separators at runtime

// Restrictions:
// Can't start or end with underscore
// const bad1 = _1000;      // SyntaxError
// const bad2 = 1000_;      // SyntaxError
// const bad3 = 1__000;     // SyntaxError (multiple consecutive)
// const bad4 = 1._000;     // SyntaxError (adjacent to decimal point)
// const bad5 = 1e_10;      // SyntaxError (adjacent to exponent)

// Valid placements
const ok1 = 1_000.500_5;  // OK
const ok2 = 0x1_2_3;      // OK
const ok3 = 1_000e5;      // OK (1000 * 10^5)
Summary: Logical assignments (||=, &&=, ??=) provide concise syntax for conditional assignment. Use ??= when preserving falsy values matters. Numeric separators improve code readability for large numbers, have no runtime effect, and work with all numeric literals (decimal, binary, hex, BigInt). Cannot place separators at start/end or adjacent to decimal/exponent.

Section 16 Summary

  • Map/Set: Collections with any key types (Map) or unique values (Set); O(1) lookup; iteration order preserved; methods: set, get, has, delete, clear
  • Proxy: Intercept object operations with 13 traps (get, set, has, apply, construct, etc.); use Reflect for proper forwarding; enables metaprogramming
  • Generators: Pausable functions with function* and yield; bidirectional communication; yield* delegation; lazy evaluation
  • Modules: Static imports (hoisted, analyzed pre-execution); dynamic imports (async, code splitting); live bindings; singletons; named/default exports
  • Private Fields: True privacy with #field; private methods/getters/setters; static private fields; #field in obj check; static initialization blocks
  • Optional Chaining: Safe property access with ?.; works with properties, methods, computed keys; short-circuits on null/undefined
  • Nullish Coalescing: Default values with ??; preserves 0, false, ''; only replaces null/undefined (unlike ||)
  • Logical Assignment: ||= (if falsy), &&= (if truthy), ??= (if null/undefined); concise conditional assignment
  • Numeric Separators: Underscore _ in numbers for readability; no runtime effect; works with all bases (decimal, binary, hex, BigInt)