ES2015+ Advanced JavaScript 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 (function*) 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. JavaScript Modules (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*andyield; 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 objcheck; 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)