// Create Setconst set = new Set();// Add valuesset.add(1);set.add(2);set.add(3);set.add(2); // Duplicate ignoredconsole.log(set.size); // 3 (only unique values)// Chainingset.add(4).add(5).add(6);// Check existenceconsole.log(set.has(3)); // trueconsole.log(set.has(10)); // false// Remove duplicates from arrayconst arr = [1, 2, 3, 2, 4, 3, 5];const unique = [...new Set(arr)]; // [1, 2, 3, 4, 5]// Iterationfor (const value of set) { console.log(value);}set.forEach(value => { console.log(value);});// Convert to arrayconst 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]);// Unionconst union = new Set([...setA, ...setB]); // {1, 2, 3, 4, 5}// Intersectionconst intersection = new Set([...setA].filter(x => setB.has(x))); // {3}// Differenceconst difference = new Set([...setA].filter(x => !setB.has(x))); // {1, 2}// Symmetric differenceconst symDiff = new Set([ ...[...setA].filter(x => !setB.has(x)), ...[...setB].filter(x => !setA.has(x))]); // {1, 2, 4, 5}// Delete and clearset.delete(3); // trueset.clear(); // All values removed
Example: Practical Map use cases
// Cache with object keysconst 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 occurrencesfunction 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 propertyfunction 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 pairsconst map2 = new Map([ ['key1', 'value1'], ['key2', 'value2'], ['key3', 'value3']]);// Convert object to Mapconst obj = {a: 1, b: 2, c: 3};const map3 = new Map(Object.entries(obj));// Convert Map to objectconst 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 loggingconst 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 proxyconst 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; // OKvalidatedUser.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 propertiesconst 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 objectfunction 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 callsfunction 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 proxyfunction 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)); // Computesconsole.log(memoFib(5)); // Cache hit// Constructor interceptionclass 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 changesfunction 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 propertiesconst 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()}.
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 exportsexport 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 listconst 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 renameexport { add2 as addition, multiply2 as multiplication };// ===== Importing =====// app.js - Import specific named exportsimport { PI, add, multiply } from './math.js';console.log(PI); // 3.14159console.log(add(2, 3)); // 5// Import with renameimport { add as sum, multiply as product } from './math.js';console.log(sum(2, 3)); // 5console.log(product(2, 3)); // 6// Import all as namespaceimport * as math from './math.js';console.log(math.PI); // 3.14159console.log(math.add(2, 3)); // 5// Import multipleimport { 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, '+')); // 8console.log(config.apiUrl); // "https://api.example.com"// Mix default and named exports// utils.jsexport default function log(msg) { console.log(msg);}export const VERSION = '1.0.0';export const DEBUG = true;// app.jsimport log, { VERSION, DEBUG } from './utils.js';log('Starting app');console.log(VERSION); // "1.0.0"
Example: Re-exports and barrel exports
// components/Button.jsexport default class Button {}// components/Input.jsexport default class Input {}// components/Form.jsexport default class Form {}// components/index.js - Barrel exportexport { default as Button } from './Button.js';export { default as Input } from './Input.js';export { default as Form } from './Form.js';// Or re-export everythingexport * from './Button.js';export * from './Input.js';export * from './Form.js';// ===== Usage =====// app.js - Single import for all componentsimport { Button, Input, Form } from './components/index.js';// Re-export with namespace// components/index.jsexport * as Button from './Button.js';export * as Input from './Input.js';// app.jsimport { Button, Input } from './components/index.js';Button.default; // Access default exportButton.someOtherExport; // Access named exports// Aggregate re-exports// api/index.jsexport * from './users.js'; // Re-export all from usersexport * from './products.js'; // Re-export all from productsexport * from './orders.js'; // Re-export all from orders// Now import all API functions from one placeimport { getUser, getProduct, getOrder } from './api/index.js';
Example: Dynamic imports
// Dynamic import returns a Promiseasync function loadModule() { const module = await import('./math.js'); console.log(module.PI); // 3.14159 console.log(module.add(2, 3)); // 5}loadModule();// Conditional loadingasync 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 handlingasync function lazyLoad() { try { const { someFunction } = await import('./heavy-module.js'); someFunction(); } catch (error) { console.error('Failed to load module:', error); }}// Code splitting in routesconst 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 destructuringbutton.addEventListener('click', async () => { const { calculate, PI } = await import('./math.js'); console.log(calculate(PI, 2, '*'));});// Load multiple modules in parallelasync function loadMultiple() { const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js') ]); // Use modules...}// Feature detection with dynamic importif ('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()); // 1000account.deposit(500); // trueconsole.log(account.getBalance()); // 1500// Private fields truly inaccessible from outsideconsole.log(account.#balance); // SyntaxErrorconsole.log(account.balance); // undefinedconsole.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)); // trueconsole.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()); // 1console.log(db1.getConnectionId()); // 1// Can't access private static fieldconsole.log(DatabaseConnection.#instance); // SyntaxError
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.
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 setlet config = {};config.timeout ||= 5000; // Assigns 5000config.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 initializationclass 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 defaultlet items = null;items ||= []; // Assigns []items.push(1, 2, 3);// Problem: replaces falsy values you want to keeplet 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 updatelet user = {name: 'Alice', admin: true};// Only update if user is adminuser.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 existslet settings = { theme: 'dark', language: 'en'};// Uppercase language if it existssettings.language &&= settings.language.toUpperCase();console.log(settings.language); // "EN"settings.missing &&= 'value';console.log(settings.missing); // undefined (no assignment)// Validation chainfunction processData(data) { // Only proceed if data exists and is valid data &&= validateData(data); data &&= transformData(data); data &&= enrichData(data); return data;}// Increment if enabledlet feature = { enabled: true, count: 5};feature.enabled &&= feature.count++;console.log(feature); // {enabled: 5, count: 6}// Disable featurefeature.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 3config2.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 defaultsfunction setDefaults(formData) { formData.name ??= 'Anonymous'; formData.age ??= 0; // Preserve explicit 0 formData.agree ??= false; // Preserve explicit false formData.email ??= ''; // Preserve explicit empty}// Nested object initializationconst state = {};state.user ??= {};state.user.preferences ??= {};state.user.preferences.theme ??= 'light';// Lazy property initializationclass Resource { #data; get data() { this.#data ??= this.#load(); return this.#data; } #load() { return {loaded: true}; }}// Array element defaultconst arr2 = [1, null, 3, undefined, 5];arr2[1] ??= 2; // Assigns 2arr2[3] ??= 4; // Assigns 4console.log(arr2); // [1, 2, 3, 4, 5]
Example: Numeric separators
// Improve readability of large numbers// Large integersconst million = 1_000_000;const billion = 1_000_000_000;const trillion = 1_000_000_000_000;console.log(million); // 1000000console.log(billion); // 1000000000// Financial calculationsconst price = 1_299.99;const salary = 75_000;const budget = 2_500_000;// Binary (bytes)const byte = 0b1111_1111; // 255const 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 notationconst lightSpeed = 299_792_458; // m/sconst plancksConstant = 6.626_070_15e-34; // J⋅sconst avogadro = 6.022_140_76e23; // mol⁻¹// BigIntconst veryLarge = 9_007_199_254_740_991n; // Max safe integerconst hugeNumber = 1_000_000_000_000_000_000_000n;// Credit card number (for display/parsing)const cardNumber = '4532_1234_5678_9010'; // String format// File sizesconst 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 placementsconst ok1 = 1_000.500_5; // OKconst ok2 = 0x1_2_3; // OKconst 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