1. JavaScript Syntax and Language Fundamentals
1.1 JavaScript Syntax Structure and Statements
| Statement Type | Syntax | Description | Example |
|---|---|---|---|
| Expression Statement | expression; |
Any valid expression followed by semicolon (ASI applies) | x = 5; 2 + 2; |
| Declaration Statement | var/let/const name; |
Declares variables with specific scope and mutability | let x; const y = 10; |
| Block Statement | { statements } |
Groups multiple statements; creates block scope for let/const | { let x = 1; } |
| Empty Statement | ; |
Does nothing; useful in loop bodies or as placeholder | while(condition); |
| Labeled Statement | label: statement |
Adds identifier to statement for break/continue targeting | loop1: for(...) {...} |
| Function Declaration | function name() {} |
Hoisted function definition, available before declaration | function add(a,b) { return a+b; } |
| Class Declaration | class Name {} |
Declares class; not hoisted like functions | class User { constructor() {} } |
| Import Statement | import x from 'mod'; |
Loads module exports; hoisted and executed first | import { func } from './module'; |
| Export Statement | export default x; |
Exposes values/functions from module | export { func, var }; |
Example: Statement types in action
// Expression statements
x = 5;
y = x + 10;
// Block statement with local scope
{
let blockScoped = 'local';
const PI = 3.14159;
}
// Labeled statement for loop control
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outer;
}
}
ASI (Automatic Semicolon Insertion): JavaScript automatically inserts semicolons in certain
cases, but relying on it can cause bugs. Always use explicit semicolons for clarity.
1.2 Variable Declarations (var, let, const)
| Declaration | Scope | Hoisting | Reassignment | Redeclaration | TDZ |
|---|---|---|---|---|---|
| var LEGACY | Function scope | Yes (undefined) | ✓ Allowed | ✓ Allowed | ✗ No |
| let ES6 | Block scope | Yes (uninitialized) | ✓ Allowed | ✗ SyntaxError | ✓ Yes |
| const ES6 | Block scope | Yes (uninitialized) | ✗ TypeError | ✗ SyntaxError | ✓ Yes |
| Feature | var | let | const |
|---|---|---|---|
| Global Object Property | ✓ Creates property on window/global | ✗ Does not create property | ✗ Does not create property |
| Loop Binding | Single binding (shared) | New binding per iteration | New binding per iteration |
| Initialization Required | ✗ Optional | ✗ Optional | ✓ Required at declaration |
| Object/Array Mutation | Allowed | Allowed | Allowed (reference is const) |
Example: Variable declaration differences
// var: function-scoped, hoisted
console.log(x); // undefined (hoisted)
var x = 5;
if (true) {
var x = 10; // Same variable
}
console.log(x); // 10
// let: block-scoped, TDZ
// console.log(y); // ReferenceError: Cannot access before initialization
let y = 5;
if (true) {
let y = 10; // Different variable
console.log(y); // 10
}
console.log(y); // 5
// const: block-scoped, immutable binding
const z = 5;
// z = 10; // TypeError: Assignment to constant
const obj = { a: 1 };
obj.a = 2; // ✓ OK: mutation allowed
// obj = {}; // ✗ TypeError: reassignment not allowed
Warning:
const creates an immutable binding, not an
immutable value. Object properties can still be modified.
1.3 Identifier Rules and Naming Conventions
| Rule | Description | Valid Examples | Invalid Examples |
|---|---|---|---|
| First Character | Letter (a-z, A-Z), underscore (_), or dollar sign ($) | _var, $elem, myVar |
1var, -name, @value |
| Subsequent Characters | Letters, digits (0-9), underscore, dollar sign | var1, _temp2, $el3 |
my-var, my.var, my var |
| Case Sensitivity | JavaScript is case-sensitive; myVar ≠ myvar | myVar, MyVar, MYVAR |
All are different variables |
| Unicode Support | Unicode letters allowed (international characters) | μ, café, 変数 |
Avoid for portability |
| Reserved Words | Cannot use JavaScript keywords as identifiers | myClass, _return |
class, return, if |
| Convention | Usage | Example | Purpose |
|---|---|---|---|
| camelCase | Variables, functions, methods | myVariable, getUserData() |
Standard for most identifiers |
| PascalCase | Classes, constructors, components | UserProfile, DataService |
Distinguishes classes from functions |
| UPPER_SNAKE_CASE | Constants, configuration values | MAX_SIZE, API_URL |
Indicates immutable values |
| _privateConvention | Private/internal members (convention) | _internalCache, _helper() |
Signals internal use only |
| #privateFields ES2022 | True private class fields | #privateValue |
Language-level privacy |
Example: Naming conventions in practice
// Constants
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
// Variables and functions (camelCase)
let userName = 'John';
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Classes (PascalCase)
class UserProfile {
#privateData; // True private field
constructor(name) {
this._internalId = Math.random(); // Convention-based private
this.#privateData = name;
}
}
// Boolean variables (descriptive prefix)
const isActive = true;
const hasPermission = false;
const canEdit = user.role === 'admin';
1.4 Comments and Documentation Syntax
| Comment Type | Syntax | Usage | Example |
|---|---|---|---|
| Single-line | // comment |
Brief explanations, inline notes | // Calculate sum |
| Multi-line | /* comment */ |
Longer explanations, block comments | /* This is a multi-line comment */ |
| JSDoc Block | /** @tag */ |
API documentation, type annotations | /** @param {string} name */ |
| Hashbang | #!/usr/bin/env node |
Shebang for executable scripts (line 1 only) | Node.js CLI scripts |
| HTML-style LEGACY | <!-- comment --> |
Legacy browser compatibility (avoid) | Only in embedded scripts |
| JSDoc Tag | Purpose | Syntax | Example |
|---|---|---|---|
| @param | Function parameter documentation | @param {type} name description |
@param {number} age User's age |
| @returns | Return value description | @returns {type} description |
@returns {boolean} True if valid |
| @typedef | Custom type definition | @typedef {Object} TypeName |
@typedef {Object} User |
| @type | Variable type annotation | @type {type} |
@type {string[]} |
| @deprecated | Mark as deprecated | @deprecated Use newFunc instead |
Signals obsolete code |
| @example | Usage examples | @example functionCall() |
Provides code samples |
| @throws | Documents exceptions | @throws {Error} description |
@throws {TypeError} |
Example: JSDoc documentation
/**
* Calculates the total price with tax
* @param {number} price - Base price of item
* @param {number} taxRate - Tax rate as decimal (e.g., 0.08)
* @returns {number} Total price including tax
* @throws {TypeError} If parameters are not numbers
* @example
* calculateTotal(100, 0.08); // Returns 108
*/
function calculateTotal(price, taxRate) {
if (typeof price !== 'number' || typeof taxRate !== 'number') {
throw new TypeError('Parameters must be numbers');
}
return price * (1 + taxRate);
}
/**
* @typedef {Object} User
* @property {string} name - User's full name
* @property {number} age - User's age
* @property {string[]} roles - User roles
*/
/** @type {User} */
const user = {
name: 'John Doe',
age: 30,
roles: ['admin', 'user']
};
1.5 Strict Mode and Its Effects
| Aspect | Non-Strict Mode | Strict Mode | Impact |
|---|---|---|---|
| Activation | Default behavior | 'use strict'; at file/function start |
Enables stricter parsing and error handling |
| Implicit Globals | Creates global variable | ReferenceError | Prevents accidental globals from typos |
| Assignment to Non-writable | Silently fails | TypeError | Throws error on read-only property assignment |
| Delete Variables | Returns false | SyntaxError | Cannot delete variables, functions, or arguments |
| Duplicate Parameters | Allowed (last wins) | SyntaxError | Prevents function param name conflicts |
| Octal Literals | 0123 allowed |
SyntaxError | Use 0o123 instead |
| this in Functions | Global object (window) | undefined |
Prevents accidental global modifications |
| with Statement | Allowed | SyntaxError | Banned due to scope ambiguity |
| eval Scope | Creates variables in surrounding scope | Own scope only | Prevents eval from polluting scope |
| Reserved Words | Some allowed as identifiers | Stricter: implements, interface, let, package, private, protected, public, static, yield |
Future-proofs code for new keywords |
Example: Strict mode effects
'use strict';
// 1. Prevents implicit globals
// mistypedVariable = 17; // ReferenceError (without strict: creates global)
// 2. Prevents duplicate parameters
// function sum(a, a, c) { } // SyntaxError (without strict: allowed)
// 3. Makes 'this' undefined in functions
function showThis() {
console.log(this); // undefined (without strict: window/global)
}
showThis();
// 4. Prevents assignment to non-writable properties
const obj = {};
Object.defineProperty(obj, 'x', { value: 42, writable: false });
// obj.x = 9; // TypeError (without strict: silently fails)
// 5. Prevents deleting variables
let x = 10;
// delete x; // SyntaxError (without strict: returns false)
// 6. Safer eval
eval('var y = 2;');
// console.log(y); // ReferenceError (without strict: y accessible)
// 7. Octal literals banned
// const octal = 0123; // SyntaxError
const octalCorrect = 0o123; // ✓ Use 0o prefix
Note: ES6 modules and classes automatically use strict mode. No need to add
'use strict'; explicitly.
1.6 JavaScript Reserved Words and Keywords
| Category | Keywords | Description |
|---|---|---|
| Control Flow | if, else, switch, case, default, break, continue, return |
Conditional execution and flow control |
| Loops | for, while, do, in, of |
Iteration constructs |
| Declarations | var, let, const, function, class |
Variable and function definitions |
| Exception Handling | try, catch, finally, throw |
Error handling constructs |
| Object-Oriented | new, this, super, extends, static |
OOP keywords |
| Type/Value | null, undefined, true, false, NaN, Infinity |
Primitive values and special values |
| Operators | typeof, instanceof, void, delete, in |
Special operators |
| Module System | import, export, from, as, default |
ES6 module syntax |
| Async | async, await, yield |
Asynchronous programming keywords |
| Other | debugger, with DEPRECATED |
Debugging and legacy features |
| Future Reserved (Strict Mode) | Status | Notes |
|---|---|---|
implements, interface, package |
Reserved | Reserved for potential future use in strict mode |
private, protected, public |
Reserved | Access modifiers reserved for future features |
enum |
Reserved (all modes) | Reserved for enumeration type (not yet implemented) |
arguments, eval |
Restricted in strict mode | Cannot be used as identifiers or assigned to |
Warning: While some keywords like
undefined and NaN are not
technically reserved, they reference global properties and should not be used as variable names to avoid
confusion.
Section 1 Summary
- JavaScript uses various statement types for different purposes (expressions, declarations, blocks)
- Use let/const over
varfor block scoping and better error detection - Follow naming conventions: camelCase for variables/functions, PascalCase for classes, UPPER_SNAKE_CASE for constants
- JSDoc comments provide type information and documentation for better IDE support
- Strict mode catches common mistakes and prevents unsafe actions (auto-enabled in modules/classes)
- Avoid using reserved keywords as identifiers; they're reserved for language features
2. Data Types and Type System
2.1 Primitive Types (string, number, boolean, null, undefined, symbol, bigint)
| Type | Description | typeof Result | Default Value | Wrapper Object |
|---|---|---|---|---|
| string | Immutable sequence of Unicode characters | "string" |
"" (empty string) |
String |
| number | 64-bit IEEE 754 floating point (±1.8×10308) | "number" |
0 |
Number |
| boolean | Logical true or false value | "boolean" |
false |
Boolean |
| null | Intentional absence of value (assignment) | "object" BUG |
N/A | None |
| undefined | Variable declared but not assigned | "undefined" |
undefined |
None |
| symbol ES6 | Unique, immutable identifier for object properties | "symbol" |
N/A | Symbol |
| bigint ES2020 | Arbitrary precision integers (beyond 253-1) | "bigint" |
0n |
BigInt |
| Type | Literal Syntax | Constructor | Special Values |
|---|---|---|---|
| string | 'text', "text", `template` |
String("value") |
Empty string "" |
| number | 42, 3.14, 1e5, 0xFF, 0o77, 0b1010 |
Number("123") |
NaN, Infinity, -Infinity |
| boolean | true, false |
Boolean(value) |
Only true and false |
| null | null |
N/A | Only null |
| undefined | undefined |
undefined or void 0 |
Only undefined |
| symbol | N/A (must use function) | Symbol("desc") |
Each symbol is unique |
| bigint | 42n, 0xFFn, 0o77n, 0b1010n |
BigInt("123") |
No decimal values |
Example: Primitive type characteristics
// Immutability: primitives are immutable
let str = "hello";
str[0] = "H"; // No effect, strings are immutable
console.log(str); // "hello"
// Primitive vs Wrapper auto-boxing
let num = 42;
console.log(num.toString()); // "42" - temporary wrapper created
// Special number values
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
// Symbols are unique
const sym1 = Symbol("desc");
const sym2 = Symbol("desc");
console.log(sym1 === sym2); // false (each is unique)
// BigInt for large integers
const bigNum = 9007199254740991n + 1n;
console.log(bigNum); // 9007199254740992n
Warning:
typeof null returns "object" due to a legacy bug. Use
value === null for null checking.
2.2 Reference Types (object, array, function)
| Type | Description | typeof Result | Mutable | Stored By |
|---|---|---|---|---|
| Object | Collection of key-value pairs (properties) | "object" |
✓ Yes | Reference |
| Array | Ordered collection with numeric indices (subtype of Object) | "object" |
✓ Yes | Reference |
| Function | Callable object with executable code | "function" |
✓ Yes (properties) | Reference |
| Date | Represents date and time (milliseconds since epoch) | "object" |
✓ Yes | Reference |
| RegExp | Regular expression pattern matcher | "object" |
✓ Yes | Reference |
| Map ES6 | Key-value pairs with any key type | "object" |
✓ Yes | Reference |
| Set ES6 | Collection of unique values | "object" |
✓ Yes | Reference |
| Behavior | Primitives | Reference Types |
|---|---|---|
| Assignment | Copies the value | Copies the reference (both point to same object) |
| Comparison (===) | Compares actual values | Compares references (same object?) |
| Mutability | Immutable (cannot change) | Mutable (can modify properties) |
| Storage | Stored directly in variable (stack) | Variable holds reference pointer (heap) |
| Passing to Functions | Pass by value (copy) | Pass by reference (can mutate original) |
Example: Primitive vs Reference behavior
// Primitives: value copied
let a = 5;
let b = a; // Copy value
b = 10;
console.log(a); // 5 (unchanged)
// References: pointer copied
let obj1 = { x: 5 };
let obj2 = obj1; // Copy reference
obj2.x = 10;
console.log(obj1.x); // 10 (both point to same object)
// Comparison
let str1 = "hello";
let str2 = "hello";
console.log(str1 === str2); // true (value comparison)
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false (different references)
console.log(arr1 === arr1); // true (same reference)
// Function parameter behavior
function modifyPrimitive(x) {
x = 100; // Local copy modified
}
let num = 5;
modifyPrimitive(num);
console.log(num); // 5 (original unchanged)
function modifyObject(obj) {
obj.value = 100; // Original object modified
}
let data = { value: 5 };
modifyObject(data);
console.log(data.value); // 100 (original changed)
2.3 Type Conversion and Coercion Rules
| Target Type | From String | From Number | From Boolean | From Object |
|---|---|---|---|---|
| String | Unchanged | "5" |
"true"/"false" |
toString() or valueOf() |
| Number | 123 or NaN |
Unchanged | 1 / 0 |
valueOf() then toString() |
| Boolean | "" → false, else true |
0, NaN → false, else true |
Unchanged | Always true |
| Conversion Method | Syntax | Use Case | Example |
|---|---|---|---|
| Explicit String | String(x), x.toString(), x + "" |
Force string conversion | String(123) → "123" |
| Explicit Number | Number(x), +x, parseInt(), parseFloat() |
Force numeric conversion | Number("123") → 123 |
| Explicit Boolean | Boolean(x), !!x |
Force boolean conversion | Boolean(0) → false |
| Implicit Coercion | Operators: +, -, *, /, == |
Automatic conversion by operators | "5" * 2 → 10 |
| Value | to String | to Number | to Boolean |
|---|---|---|---|
| undefined | "undefined" |
NaN |
false |
| null | "null" |
0 |
false |
| true | "true" |
1 |
true |
| false | "false" |
0 |
false |
| "" | "" |
0 |
false |
| "123" | "123" |
123 |
true |
| "abc" | "abc" |
NaN |
true |
| 0, -0 | "0" |
0 |
false |
| NaN | "NaN" |
NaN |
false |
| Infinity | "Infinity" |
Infinity |
true |
| {}, [] | "[object Object]", "" |
NaN, 0 |
true |
Example: Type coercion in action
// String coercion (+ with string)
console.log("5" + 3); // "53" (number to string)
console.log("5" + true); // "5true"
// Numeric coercion (other operators)
console.log("5" - 3); // 2 (string to number)
console.log("5" * "2"); // 10
console.log(true + 1); // 2 (true → 1)
console.log(false + 1); // 1 (false → 0)
// Boolean coercion
console.log(Boolean("")); // false
console.log(Boolean(0)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean([])); // true (object)
console.log(Boolean({})); // true (object)
// Explicit conversions
console.log(Number("123")); // 123
console.log(Number("12.5")); // 12.5
console.log(Number("abc")); // NaN
console.log(parseInt("123px")); // 123 (parses until non-digit)
console.log(String(123)); // "123"
console.log(+"42"); // 42 (unary +)
// Falsy values (6 total)
if (!undefined || !null || !0 || !NaN || !"" || !false) {
console.log("All falsy values");
}
Falsy Values (6 total):
false, 0, "", null,
undefined, NaN. Everything else is truthy (including [], {},
"0", "false").
2.4 Type Checking (typeof, instanceof, Array.isArray)
| Operator/Method | Syntax | Returns | Use Case | Limitations |
|---|---|---|---|---|
| typeof | typeof value |
String type name | Check primitive types | typeof null === "object" (bug); arrays return "object" |
| instanceof | obj instanceof Constructor |
Boolean | Check prototype chain | Doesn't work across frames/windows; primitives always false |
| Array.isArray() | Array.isArray(value) |
Boolean | Reliable array detection | Only for arrays |
| Object.prototype.toString | Object.prototype.toString.call(value) |
"[object Type]" |
Most reliable type checking | Verbose syntax |
| constructor | value.constructor === Type |
Boolean | Check constructor | Can be overridden; fails for null/undefined |
| Value | typeof | instanceof | Best Detection Method |
|---|---|---|---|
42 |
"number" |
N/A (primitive) | typeof x === "number" |
"text" |
"string" |
N/A (primitive) | typeof x === "string" |
true |
"boolean" |
N/A (primitive) | typeof x === "boolean" |
null |
"object" ⚠️ |
false | x === null |
undefined |
"undefined" |
false | x === undefined or typeof x === "undefined" |
[] |
"object" |
true (Array) | Array.isArray(x) |
{} |
"object" |
true (Object) | typeof x === "object" && x !== null |
function(){} |
"function" |
true (Function) | typeof x === "function" |
Symbol() |
"symbol" |
N/A (primitive) | typeof x === "symbol" |
42n |
"bigint" |
N/A (primitive) | typeof x === "bigint" |
Example: Type checking techniques
// typeof: basic primitive checking
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 42n); // "bigint"
console.log(typeof null); // "object" ⚠️ (legacy bug)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"
// instanceof: prototype chain checking
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Date() instanceof Date); // true
console.log(42 instanceof Number); // false (primitive)
// Array.isArray: reliable array detection
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("array")); // false
// Object.prototype.toString: most reliable
const toString = Object.prototype.toString;
console.log(toString.call([])); // "[object Array]"
console.log(toString.call({})); // "[object Object]"
console.log(toString.call(null)); // "[object Null]"
console.log(toString.call(undefined)); // "[object Undefined]"
console.log(toString.call(new Date())); // "[object Date]"
console.log(toString.call(/regex/)); // "[object RegExp]"
// Utility type checking function
function getType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
console.log(getType(null)); // "null"
console.log(getType([])); // "array"
console.log(getType(42)); // "number"
2.5 Symbol Type and Well-known Symbols
| Feature | Description | Syntax | Use Case |
|---|---|---|---|
| Symbol Creation | Creates unique identifier | Symbol("description") |
Unique object property keys |
| Symbol Registry | Global shared symbols | Symbol.for("key") |
Cross-realm symbol sharing |
| Symbol.keyFor() | Get key from registry symbol | Symbol.keyFor(sym) |
Retrieve symbol key name |
| Symbol Properties | Hidden from enumeration | obj[sym] = value |
Private-like properties (not truly private) |
| Well-known Symbol | Purpose | Usage |
|---|---|---|
| Symbol.iterator | Defines default iterator | Used by for...of loops, spread operator |
| Symbol.asyncIterator | Defines async iterator | Used by for await...of loops |
| Symbol.toStringTag | Customizes toString() output |
Changes Object.prototype.toString.call() result |
| Symbol.toPrimitive | Defines type coercion behavior | Controls conversion to primitive types |
| Symbol.hasInstance | Customizes instanceof |
Overrides instanceof behavior |
| Symbol.species | Constructor for derived objects | Used in Array/Promise methods |
| Symbol.match, matchAll | String matching behavior | Used by String.prototype.match() |
| Symbol.replace, search, split | String manipulation behavior | Used by respective string methods |
Example: Symbol usage and well-known symbols
// Basic symbol creation (each is unique)
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false (unique)
// Symbols as object properties
const id = Symbol("id");
const user = {
name: "John",
[id]: 123 // Symbol as computed property
};
console.log(user[id]); // 123
console.log(Object.keys(user)); // ["name"] (symbol hidden)
// Symbol registry (shared across realm)
const globalSym1 = Symbol.for("app.id");
const globalSym2 = Symbol.for("app.id");
console.log(globalSym1 === globalSym2); // true (same symbol)
console.log(Symbol.keyFor(globalSym1)); // "app.id"
// Well-known symbols: Symbol.iterator
const iterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index],
done: index++ >= this.data.length
})
};
}
};
console.log([...iterable]); // [1, 2, 3]
// Symbol.toStringTag
class CustomClass {
get [Symbol.toStringTag]() {
return "CustomClass";
}
}
const obj = new CustomClass();
console.log(Object.prototype.toString.call(obj)); // "[object CustomClass]"
// Symbol.toPrimitive
const obj2 = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 42;
if (hint === "string") return "hello";
return true;
}
};
console.log(+obj2); // 42 (number hint)
console.log(`${obj2}`); // "hello" (string hint)
console.log(obj2 + ""); // "true" (default hint)
2.6 BigInt for Large Integer Operations
| Feature | Syntax | Description | Example |
|---|---|---|---|
| BigInt Literal | value + "n" |
Append 'n' to integer literal | 123n, 0xFFn, 0o77n, 0b1010n |
| BigInt Constructor | BigInt(value) |
Convert number/string to BigInt | BigInt("9007199254740991") |
| Range | Arbitrary precision | No upper/lower limit (memory-limited) | Can represent integers beyond 253-1 |
| Type | typeof x |
Returns "bigint" |
typeof 42n === "bigint" |
| Operation | BigInt Support | Example | Notes |
|---|---|---|---|
| Arithmetic | +, -, *, /, %, ** |
10n + 20n = 30n |
Both operands must be BigInt |
| Comparison | <, >, <=, >=, ==, != |
10n < 20n, 10n == 10 |
== coerces; === is strict |
| Bitwise | &, |, ^, ~, <<, >> |
5n & 3n = 1n |
>>> not supported (unsigned) |
| Division | / (integer division) |
7n / 2n = 3n |
Truncates decimal (rounds toward zero) |
| Unary Minus | -x |
-42n = -42n |
Negation supported |
| Unary Plus | ❌ Not supported | +42n ❌ TypeError |
Use Number() to convert |
| Limitation | Description | Workaround |
|---|---|---|
| No Mixed Operations | Cannot mix BigInt with Number | Explicitly convert: 10n + BigInt(5) or Number(10n) + 5 |
| No Decimal Values | BigInt only for integers | Use Number for decimal/floating-point |
| Math Object | Math.* methods don't support BigInt |
Implement custom logic or convert to Number |
| JSON Serialization | JSON.stringify() throws TypeError |
Custom replacer: JSON.stringify(obj, (k,v) => typeof v === 'bigint' ? v.toString() : v)
|
| Implicit Coercion | No automatic type coercion | Always explicit conversion required |
Example: BigInt operations and limitations
// Creating BigInts
const big1 = 9007199254740991n;
const big2 = BigInt("9007199254740991");
const big3 = BigInt(Number.MAX_SAFE_INTEGER);
// Arithmetic operations
console.log(100n + 50n); // 150n
console.log(100n - 50n); // 50n
console.log(100n * 2n); // 200n
console.log(100n / 3n); // 33n (integer division, truncates)
console.log(100n % 3n); // 1n
console.log(2n ** 100n); // Very large number (2^100)
// Comparison
console.log(10n === 10); // false (strict equality)
console.log(10n == 10); // true (loose equality with coercion)
console.log(10n < 20n); // true
console.log(5n > 3); // true (coercion in comparison)
// Limitations
// console.log(10n + 5); // ❌ TypeError: Cannot mix BigInt and Number
console.log(10n + BigInt(5)); // ✓ 15n (explicit conversion)
console.log(Number(10n) + 5); // ✓ 15 (convert to Number)
// console.log(+10n); // ❌ TypeError: Cannot convert BigInt
console.log(Number(10n)); // ✓ 10 (explicit conversion)
// Bitwise operations
console.log(5n & 3n); // 1n (binary AND)
console.log(5n | 3n); // 7n (binary OR)
console.log(5n ^ 3n); // 6n (binary XOR)
console.log(5n << 2n); // 20n (left shift)
// Use case: Precise large integer calculations
const largeNumber = 9007199254740992n; // Beyond Number.MAX_SAFE_INTEGER
console.log(largeNumber + 1n); // 9007199254740993n (exact)
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992 (precision lost)
Warning: BigInt cannot be mixed with regular numbers in arithmetic operations. Always
explicitly convert using
BigInt() or Number().
Section 2 Summary
- JavaScript has 7 primitive types: string, number, boolean, null, undefined, symbol, bigint
- Primitives are immutable and passed by value; reference types are mutable and passed by reference
- Type coercion happens implicitly with operators; 6 falsy values exist:
false, 0, "", null, undefined, NaN - Use
typeoffor primitives,Array.isArray()for arrays, andinstanceoffor prototype checking - Symbols provide unique identifiers for object properties; well-known symbols customize language behavior
- BigInt enables arbitrary-precision integer math beyond Number.MAX_SAFE_INTEGER (253-1)
3. Variables, Scope, and Closure
3.1 Variable Hoisting and Temporal Dead Zone
| Declaration | Hoisting Behavior | Initialization | TDZ | Access Before Declaration |
|---|---|---|---|---|
| var | Hoisted to function/global scope | Initialized to undefined |
❌ No TDZ | Returns undefined |
| let | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| const | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| function (declaration) | Fully hoisted with definition | Fully initialized | ❌ No TDZ | Callable before declaration |
| function (expression) | Variable hoisted only | Depends on var/let/const | Depends on var/let/const | Not callable (undefined or TDZ) |
| class | Hoisted to block scope | Not initialized | ✓ Has TDZ | ReferenceError |
| Concept | Description | Key Points |
|---|---|---|
| Hoisting | JavaScript moves declarations to top of their scope during compilation | Only declarations hoisted, not initializations |
| Temporal Dead Zone (TDZ) | Time between entering scope and variable initialization where access causes error | Exists for let, const, and class |
| TDZ Start | Beginning of enclosing block scope | Not visible in code; conceptual time period |
| TDZ End | Point where variable is declared and initialized | Variable becomes usable after this point |
Example: Hoisting and TDZ behavior
// var hoisting: declaration hoisted, initialized to undefined
console.log(x); // undefined (no error)
var x = 5;
console.log(x); // 5
// let/const TDZ: declaration hoisted but not initialized
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
console.log(y); // 10
// Function declaration: fully hoisted
greet(); // "Hello" (works before declaration)
function greet() {
console.log("Hello");
}
// Function expression: only variable hoisted
// sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi");
};
sayHi(); // "Hi"
// TDZ example with block scope
{
// TDZ starts here for 'temp'
// console.log(temp); // ReferenceError
// const result = doSomething(temp); // ReferenceError
let temp = 5; // TDZ ends here
console.log(temp); // 5 (now accessible)
}
// Class TDZ
// const p = new Person(); // ReferenceError
class Person {
constructor(name) { this.name = name; }
}
const p = new Person("John"); // Works after declaration
Note: TDZ prevents using variables before initialization, catching potential bugs. Always
declare variables at the top of their scope for clarity.
3.2 Scope Types (Global, Function, Block, Module)
| Scope Type | Created By | Variables | Lifetime | Access |
|---|---|---|---|---|
| Global Scope | Outside all functions/blocks | var, let, const, functions |
Entire program execution | Accessible everywhere |
| Function Scope | Function declarations/expressions | var, parameters, inner functions |
Function execution | Within function and nested functions |
| Block Scope | { } braces, loops, if/switch |
let, const, class |
Block execution | Within block only |
| Module Scope | ES6 modules (.js files with import/export) |
All declarations in module file | Module lifetime | Isolated; explicit exports required |
| Catch Block Scope | catch(error) { } |
Error parameter, let, const |
Catch block execution | Within catch block only |
| Declaration | Global Scope | Function Scope | Block Scope |
|---|---|---|---|
| var | ✓ Function or global | ✓ Function-scoped | ❌ Ignores blocks |
| let | ✓ Global (no window property) | ✓ Function-scoped | ✓ Block-scoped |
| const | ✓ Global (no window property) | ✓ Function-scoped | ✓ Block-scoped |
| function | ✓ Global | ✓ Function-scoped | ❌ Block-scoped in strict mode only |
Example: Different scope types
// Global scope
var globalVar = 'global var';
let globalLet = 'global let';
const globalConst = 'global const';
function demoScopes() {
// Function scope
var functionVar = 'function scoped';
let functionLet = 'function scoped';
console.log(globalVar); // Accessible
if (true) {
// Block scope
var blockVar = 'not block scoped (var)';
let blockLet = 'block scoped';
const blockConst = 'block scoped';
console.log(functionVar); // Accessible from outer function
console.log(blockLet); // Accessible within block
}
console.log(blockVar); // Accessible (var ignores blocks)
// console.log(blockLet); // ReferenceError (block-scoped)
// Loop scope
for (let i = 0; i < 3; i++) {
// 'i' is block-scoped to this loop
setTimeout(() => console.log(i), 100); // Prints 0, 1, 2
}
for (var j = 0; j < 3; j++) {
// 'j' is function-scoped (shared)
setTimeout(() => console.log(j), 100); // Prints 3, 3, 3
}
}
// Module scope (in ES6 modules)
// Variables not automatically global
// export const moduleVar = 'exported';
// const privateVar = 'not exported';
// Catch block scope
try {
throw new Error('test');
} catch (error) {
// 'error' is scoped to catch block
let catchVar = 'catch scoped';
console.log(error.message);
}
// console.log(error); // ReferenceError
// console.log(catchVar); // ReferenceError
Warning: Global variables create properties on the global object (
window in
browsers) with var, but not with let/const. Avoid global variables to
prevent naming conflicts.
3.3 Lexical Scope and Scope Chain
| Concept | Description | Behavior |
|---|---|---|
| Lexical Scope | Scope determined by code location (where functions are written) | Inner functions access outer function variables |
| Scope Chain | Hierarchy of nested scopes searched for variable resolution | Searches from inner → outer until found or reaches global |
| Static Scoping | Another name for lexical scoping (opposite of dynamic scoping) | Scope determined at write-time, not runtime |
| Outer Environment Reference | Link from execution context to parent scope | Created when function is defined, not when called |
| Variable Lookup Process | Step | Action |
|---|---|---|
| Step 1 | Current Scope | Check if variable exists in current local scope |
| Step 2 | Parent Scope | If not found, move to enclosing (parent) scope |
| Step 3 | Repeat | Continue up the scope chain through ancestors |
| Step 4 | Global Scope | Check global scope as last resort |
| Step 5 | Not Found | ReferenceError if not found anywhere in chain |
Example: Lexical scope and scope chain
// Scope chain demonstration
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
const innerVar = 'inner';
// Scope chain: inner → middle → outer → global
console.log(innerVar); // Found in inner scope
console.log(middleVar); // Found in middle scope (parent)
console.log(outerVar); // Found in outer scope (grandparent)
console.log(globalVar); // Found in global scope
// Variable lookup order:
// 1. Check inner scope
// 2. Check middle scope
// 3. Check outer scope
// 4. Check global scope
// 5. ReferenceError if not found
}
inner();
// console.log(innerVar); // ReferenceError (not in scope chain)
}
middle();
}
outer();
// Lexical scope is determined by code structure
function makeCounter() {
let count = 0; // In outer function scope
return function() {
// Lexically scoped to access 'count' from parent
return ++count;
};
}
const counter1 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
const counter2 = makeCounter();
console.log(counter2()); // 1 (separate scope chain)
// Scope determined at definition, not invocation
let x = 'global x';
function showX() {
console.log(x); // Lexically bound to global 'x'
}
function demo() {
let x = 'local x';
showX(); // Prints "global x" (not "local x")
// Scope determined where showX was defined
}
demo();
Note: Lexical scope is determined by where the function is written, not where it's called. This enables closures
and predictable variable access.
3.4 Variable Shadowing and Name Resolution
| Concept | Description | Behavior | Recommendation |
|---|---|---|---|
| Variable Shadowing | Inner variable hides outer variable with same name | Inner scope variable takes precedence | Avoid shadowing; use different names |
| Name Resolution | Process of finding which variable identifier refers to | Searches scope chain from inner to outer | Use descriptive, unique names |
| Parameter Shadowing | Function parameter shadows outer variable | Parameter accessible, outer variable hidden | Common pattern; usually intentional |
| Block Shadowing | Block-scoped variable shadows outer scope | Only within block; outer restored after | Use for temporary local variables |
| Shadowing Type | Outer | Inner | Allowed? | Behavior |
|---|---|---|---|---|
| var shadows var | var | var | ✓ Yes | Redeclaration in same scope or shadowing in nested |
| let/const shadows var | var | let/const | ✓ Yes | Inner hides outer; outer unchanged |
| var shadows let/const | let/const | var | Depends | Error if same function scope; OK if nested function |
| let/const shadows let/const | let/const | let/const | ✓ In different block | ✗ SyntaxError in same block |
| Parameter shadowing | Any | Parameter | ✓ Yes | Parameter has priority in function |
Example: Variable shadowing scenarios
// Basic shadowing
let x = 'outer';
function demo() {
let x = 'inner'; // Shadows outer 'x'
console.log(x); // "inner"
}
demo();
console.log(x); // "outer" (outer unchanged)
// Parameter shadowing
let value = 100;
function processValue(value) { // Parameter shadows outer 'value'
console.log(value); // Uses parameter, not outer variable
value = 200; // Modifies parameter only
}
processValue(50); // Prints 50
console.log(value); // 100 (outer unchanged)
// Block shadowing
let count = 10;
if (true) {
let count = 20; // Shadows outer 'count' in block
console.log(count); // 20
}
console.log(count); // 10 (outer restored after block)
// Nested function shadowing
function outer() {
let name = 'outer';
function inner() {
let name = 'inner'; // Shadows parent function's 'name'
console.log(name); // "inner"
}
inner();
console.log(name); // "outer"
}
outer();
// Loop variable shadowing
for (let i = 0; i < 3; i++) {
console.log(i); // Loop 'i'
for (let i = 0; i < 2; i++) {
console.log(' ' + i); // Inner loop 'i' shadows outer
}
}
// Problematic shadowing to avoid
function calculateTotal() {
let total = 0;
if (true) {
let total = 100; // Shadowing can be confusing
// Which 'total' is which?
}
return total; // Returns 0, not 100
}
Warning: While shadowing is allowed, excessive use can make code confusing. Use distinct
variable names for clarity unless shadowing is intentional (e.g., function parameters).
3.5 Closure Patterns and Memory Management
| Concept | Definition | Key Characteristics |
|---|---|---|
| Closure | Function bundled with its lexical environment (surrounding state) | Retains access to outer scope variables even after outer function returns |
| Closure Creation | Created every time a function is created | Inner function "closes over" variables from outer scope |
| Closure Use Cases | Data privacy, factory functions, callbacks, event handlers | Maintains state between function calls |
| Memory Retention | Closed-over variables remain in memory | Can cause memory leaks if not managed properly |
| Closure Pattern | Use Case | Example Structure |
|---|---|---|
| Private Variables | Encapsulation, data hiding | Function returns methods that access private variables |
| Factory Functions | Create objects with private state | Function returns object with methods (closures) |
| Module Pattern | Namespace management, public/private API | IIFE returns object with public methods |
| Callbacks/Event Handlers | Preserve context for async operations | Function captures variables for later execution |
| Currying/Partial Application | Function transformation, configuration | Function returns function with captured arguments |
| Memoization | Performance optimization, caching | Closure maintains cache of computed values |
Example: Common closure patterns
// 1. Private variables pattern
function createCounter() {
let count = 0; // Private variable
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined (private)
// 2. Factory function pattern
function createUser(name) {
let _name = name; // Private
let _sessions = 0;
return {
getName: () => _name,
login: () => ++_sessions,
getSessions: () => _sessions
};
}
const user1 = createUser("Alice");
const user2 = createUser("Bob");
user1.login();
console.log(user1.getSessions()); // 1
console.log(user2.getSessions()); // 0
// 3. Module pattern with IIFE
const calculator = (function() {
let history = []; // Private state
return {
add: (a, b) => {
const result = a + b;
history.push(`${a} + ${b} = ${result}`);
return result;
},
getHistory: () => [...history] // Return copy
};
})();
calculator.add(5, 3);
console.log(calculator.getHistory()); // ["5 + 3 = 8"]
// 4. Callback closure preserving context
function fetchDataWithRetry(url, retries) {
let attempts = 0; // Captured by callback
function attemptFetch() {
fetch(url)
.catch(error => {
if (++attempts < retries) {
console.log(`Retry ${attempts}`);
attemptFetch(); // Closure accesses 'attempts'
}
});
}
attemptFetch();
}
// 5. Currying with closures
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const multiplyBy2 = multiply(2);
const multiplyBy2And3 = multiplyBy2(3);
console.log(multiplyBy2And3(4)); // 24
// 6. Memoization pattern
function memoize(fn) {
const cache = {}; // Closure maintains cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n - 1));
console.log(factorial(5)); // Computed: 120
console.log(factorial(5)); // Cached: 120
| Memory Consideration | Issue | Solution |
|---|---|---|
| Retained Variables | Closed-over variables can't be garbage collected | Clear references when done (obj = null) |
| Event Listeners | Closure in listener prevents GC of entire scope | Remove listeners: removeEventListener |
| Timers/Intervals | Callback closures keep references alive | Clear timers: clearTimeout/clearInterval |
| Large Data Structures | Closure inadvertently captures large objects | Extract only needed data before creating closure |
Note: Closures are powerful but retain references to their outer scope. Be mindful of memory
usage, especially with long-lived closures or large captured variables.
3.6 Module Scope and Variable Isolation
| Module Feature | Description | Benefit |
|---|---|---|
| Module Scope | Each ES6 module has its own scope (file-level) | Variables don't pollute global scope by default |
| Variable Isolation | Module variables are private unless explicitly exported | Prevents naming conflicts between modules |
| Strict Mode | Modules automatically run in strict mode | No need for 'use strict'; directive |
| Top-level this | this is undefined at module top level |
Not bound to global object (safer) |
| Static Imports | Import declarations hoisted and executed first | Enables static analysis and tree-shaking |
| Module Pattern | Syntax | Use Case |
|---|---|---|
| Named Export | export const x = 1; |
Export multiple items from module |
| Default Export | export default function() {} |
Main export from module (one per module) |
| Named Import | import { x, y } from './mod'; |
Import specific exports |
| Default Import | import mod from './mod'; |
Import default export |
| Namespace Import | import * as mod from './mod'; |
Import all exports as object |
| Re-export | export { x } from './mod'; |
Export from another module (barrel pattern) |
| Dynamic Import | import('./mod').then(...) |
Load modules conditionally/lazily |
Example: Module scope and isolation
// ===== math.js (Module A) =====
// Private variables (not exported)
const PI = 3.14159;
let calculationCount = 0;
// Private helper function
function log(operation) {
calculationCount++;
console.log(`Operation: ${operation}, Count: ${calculationCount}`);
}
// Named exports (public API)
export function add(a, b) {
log('add');
return a + b;
}
export function multiply(a, b) {
log('multiply');
return a * b;
}
export const TAU = 2 * PI; // Export constant
// Default export
export default function calculate(expr) {
return eval(expr); // Example only
}
// ===== user.js (Module B) =====
// Module variables are isolated
const userCount = 0; // Won't conflict with other modules
class User {
constructor(name) {
this.name = name;
}
}
export { User, userCount };
// ===== app.js (Main module) =====
// Import from math.js
import calculate, { add, multiply, TAU } from './math.js';
// Import from user.js
import { User, userCount } from './user.js';
// Module-scoped variables (private to this module)
const appName = 'MyApp';
let config = { debug: true };
console.log(add(5, 3)); // 8
console.log(multiply(2, 4)); // 8
console.log(TAU); // 6.28318
// Cannot access private variables from math.js
// console.log(PI); // ReferenceError
// console.log(calculationCount); // ReferenceError
const user = new User('Alice');
// Top-level 'this' is undefined in modules
console.log(this); // undefined (not window)
// ===== Dynamic imports =====
async function loadModule() {
if (config.debug) {
const debugModule = await import('./debug.js');
debugModule.log('Debug mode enabled');
}
}
// ===== Barrel export pattern (index.js) =====
// Re-export from multiple modules
export { add, multiply } from './math.js';
export { User } from './user.js';
export { default as calculate } from './math.js';
Note: ES6 modules provide true encapsulation with file-level scope. Variables are private by
default, eliminating need for IIFE patterns and reducing global scope pollution.
Section 3 Summary
- Hoisting moves declarations to top of scope;
let/const/classhave TDZ (ReferenceError before initialization) - 4 scope types: Global (everywhere), Function (var), Block (let/const), Module (file-level isolation)
- Lexical scope determined by code structure; scope chain searches inner → outer → global for variables
- Variable shadowing occurs when inner scope variable hides outer one; use distinct names for clarity
- Closures retain access to outer scope variables; enable private state but require memory management
- ES6 modules provide true isolation with file-level scope; variables private unless exported
4. Operators and Expressions Reference
4.1 Arithmetic Operators and Math Operations
| Operator | Name | Description | Example | Result |
|---|---|---|---|---|
| + | Addition / Unary Plus | Adds numbers or concatenates strings; converts to number | 5 + 3, +"42" |
8, 42 |
| - | Subtraction / Unary Minus | Subtracts numbers; negates value | 5 - 3, -5 |
2, -5 |
| * | Multiplication | Multiplies numbers | 5 * 3 |
15 |
| / | Division | Divides numbers (floating-point result) | 10 / 3 |
3.3333... |
| % | Remainder / Modulo | Returns remainder of division | 10 % 3 |
1 |
| ** | Exponentiation ES2016 | Raises to power | 2 ** 3 |
8 |
| ++ | Increment | Increases by 1 (prefix or postfix) | ++x, x++ |
Pre: return new; Post: return old |
| -- | Decrement | Decreases by 1 (prefix or postfix) | --x, x-- |
Pre: return new; Post: return old |
| Operation Type | Behavior | Special Cases |
|---|---|---|
| Number + Number | Numeric addition | 5 + 3 = 8 |
| String + Any | String concatenation | "5" + 3 = "53", "a" + true = "atrue" |
| Division by Zero | Returns Infinity |
5 / 0 = Infinity, -5 / 0 = -Infinity |
| 0 / 0 | Returns NaN |
Indeterminate result |
| NaN in Operations | Propagates NaN |
NaN + 5 = NaN, NaN * 2 = NaN |
| Infinity Operations | Mathematical infinity rules | Infinity + 1 = Infinity, Infinity * -1 = -Infinity |
Example: Arithmetic operators and edge cases
// Basic arithmetic
console.log(10 + 5); // 15
console.log(10 - 5); // 5
console.log(10 * 5); // 50
console.log(10 / 5); // 2
console.log(10 % 3); // 1 (remainder)
console.log(2 ** 3); // 8 (exponentiation)
// Increment/Decrement
let x = 5;
console.log(x++); // 5 (returns old value, then increments)
console.log(x); // 6
console.log(++x); // 7 (increments, then returns new value)
console.log(x--); // 7 (returns old value, then decrements)
console.log(--x); // 5 (decrements, then returns new value)
// String concatenation with +
console.log("Hello" + " " + "World"); // "Hello World"
console.log("5" + 3); // "53" (string concatenation)
console.log(5 + "3"); // "53"
console.log("5" + 3 + 2); // "532"
console.log(5 + 3 + "2"); // "82" (left to right: 5+3=8, then 8+"2"="82")
// Type coercion with other operators
console.log("10" - 5); // 5 (string to number)
console.log("10" * "2"); // 20
console.log("10" / "2"); // 5
console.log("10" % 3); // 1
// Unary operators
console.log(+"42"); // 42 (string to number)
console.log(+"abc"); // NaN
console.log(-"5"); // -5
// Special values
console.log(5 / 0); // Infinity
console.log(-5 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(NaN + 10); // NaN (NaN propagates)
console.log(Infinity - 1); // Infinity
4.2 Assignment Operators and Compound Assignment
| Operator | Name | Equivalent | Example | Result |
|---|---|---|---|---|
| = | Simple Assignment | N/A | x = 5 |
x is 5 |
| += | Addition Assignment | x = x + y |
x += 3 |
Adds and assigns |
| -= | Subtraction Assignment | x = x - y |
x -= 3 |
Subtracts and assigns |
| *= | Multiplication Assignment | x = x * y |
x *= 3 |
Multiplies and assigns |
| /= | Division Assignment | x = x / y |
x /= 3 |
Divides and assigns |
| %= | Remainder Assignment | x = x % y |
x %= 3 |
Remainder and assigns |
| **= | Exponentiation Assignment | x = x ** y |
x **= 3 |
Exponentiates and assigns |
| <<= | Left Shift Assignment | x = x << y |
x <<= 2 |
Left shift and assigns |
| >>= | Right Shift Assignment | x = x >> y |
x >>= 2 |
Right shift and assigns |
| >>>= | Unsigned Right Shift Assignment | x = x >>> y |
x >>>= 2 |
Unsigned shift and assigns |
| &= | Bitwise AND Assignment | x = x & y |
x &= 3 |
AND and assigns |
| |= | Bitwise OR Assignment | x = x | y |
x |= 3 |
OR and assigns |
| ^= | Bitwise XOR Assignment | x = x ^ y |
x |= 3 |
XOR and assigns |
| Feature | Description | Example |
|---|---|---|
| Destructuring Assignment | Unpack values from arrays/objects | [a, b] = [1, 2], {x, y} = obj |
| Multiple Assignment | Chain assignments (right-to-left) | a = b = c = 5 (all get 5) |
| Return Value | Assignment returns assigned value | y = (x = 5) (both are 5) |
| Compound Benefits | Shorter, clearer, evaluates left side once | arr[i++] += 5 increments i once |
Example: Assignment operators
// Simple assignment
let x = 10;
// Compound assignments
x += 5; // x = x + 5; x is now 15
x -= 3; // x = x - 3; x is now 12
x *= 2; // x = x * 2; x is now 24
x /= 4; // x = x / 4; x is now 6
x %= 5; // x = x % 5; x is now 1
x **= 3; // x = x ** 3; x is now 1
// String concatenation with +=
let str = "Hello";
str += " World"; // str is "Hello World"
// Multiple assignment (right-to-left)
let a, b, c;
a = b = c = 5; // All are 5
console.log(a, b, c); // 5 5 5
// Assignment returns value
let y = (x = 10); // x is 10, y is 10
console.log(x, y); // 10 10
// Destructuring assignment
let [p, q] = [1, 2];
console.log(p, q); // 1 2
let {name, age} = {name: "John", age: 30};
console.log(name, age); // "John" 30
// Compound assignment with side effects
let arr = [10, 20, 30];
let i = 0;
arr[i++] += 5; // arr[0] becomes 15, then i increments
console.log(arr[0], i); // 15 1
// Swapping with destructuring
let m = 1, n = 2;
[m, n] = [n, m]; // Swap
console.log(m, n); // 2 1
4.3 Comparison Operators and Equality Rules
| Operator | Name | Description | Type Coercion | Example |
|---|---|---|---|---|
| == | Equality (loose) | Checks value equality with type coercion | ✓ Yes | 5 == "5" → true |
| === | Strict Equality | Checks value and type equality | ✗ No | 5 === "5" → false |
| != | Inequality (loose) | Checks value inequality with coercion | ✓ Yes | 5 != "6" → true |
| !== | Strict Inequality | Checks value or type inequality | ✗ No | 5 !== "5" → true |
| < | Less Than | Checks if left < right | ✓ Yes | 5 < 10 → true |
| > | Greater Than | Checks if left > right | ✓ Yes | 10 > 5 → true |
| <= | Less Than or Equal | Checks if left ≤ right | ✓ Yes | 5 <= 5 → true |
| >= | Greater Than or Equal | Checks if left ≥ right | ✓ Yes | 10 >= 10 → true |
| Comparison | == (loose) | === (strict) | Reason |
|---|---|---|---|
5 vs "5" |
true |
false |
String coerced to number |
0 vs false |
true |
false |
Boolean coerced to number |
null vs undefined |
true |
false |
Special case: only equal to each other |
"" vs 0 |
true |
false |
Empty string coerced to 0 |
NaN vs NaN |
false |
false |
NaN not equal to itself (use isNaN()) |
[] vs [] |
false |
false |
Different object references |
0 vs -0 |
true |
true |
Considered equal (use Object.is() to distinguish) |
Example: Equality and comparison operators
// Strict equality (===) - recommended
console.log(5 === 5); // true
console.log(5 === "5"); // false (different types)
console.log(null === null); // true
console.log(undefined === undefined); // true
// Loose equality (==) - with type coercion
console.log(5 == "5"); // true (string coerced to number)
console.log(0 == false); // true (boolean to number)
console.log("" == 0); // true (string to number)
console.log(null == undefined); // true (special case)
// Inequality
console.log(5 !== "5"); // true (strict)
console.log(5 != "6"); // true (loose)
// Relational operators
console.log(5 < 10); // true
console.log(10 > 5); // true
console.log(5 <= 5); // true
console.log(10 >= 10); // true
// String comparison (lexicographic)
console.log("apple" < "banana"); // true (a < b)
console.log("10" < "9"); // true (string comparison: "1" < "9")
console.log(10 < "9"); // false (number comparison: 10 > 9)
// Special cases
console.log(NaN === NaN); // false (use Number.isNaN())
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true (correct way)
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false (distinguishes +0 and -0)
// Object comparison (reference)
const obj1 = {a: 1};
const obj2 = {a: 1};
const obj3 = obj1;
console.log(obj1 === obj2); // false (different references)
console.log(obj1 === obj3); // true (same reference)
// Array comparison
console.log([1,2] == [1,2]); // false (different references)
console.log([1,2] === [1,2]); // false
// Tricky comparisons to avoid
console.log([] == ![]); // true (![] is false, [] coerced to "")
console.log([] == false); // true
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("0" == 0); // true
console.log("0" == false); // true
Warning: Always use
=== and !== (strict equality) to avoid unexpected
type coercion bugs. Use == only when you specifically need type coercion.
4.4 Logical Operators and Short-circuit Evaluation
| Operator | Name | Returns | Short-circuits | Use Case |
|---|---|---|---|---|
| && | Logical AND | First falsy value or last value | ✓ If left is falsy | Both conditions must be true |
| || | Logical OR | First truthy value or last value | ✓ If left is truthy | At least one condition must be true |
| ! | Logical NOT | Boolean (inverted) | ✗ No | Inverts truthiness |
| !! | Double NOT | Boolean conversion | ✗ No | Convert to boolean explicitly |
| Expression | Result | Explanation |
|---|---|---|
| true && true | true |
Both truthy → returns last value |
| true && false | false |
Right is falsy → returns false |
| false && true | false |
Short-circuits at false (left) |
| 5 && "hello" | "hello" |
Both truthy → returns last |
| 0 && "hello" | 0 |
First falsy → returns 0 |
| true || false | true |
Short-circuits at first truthy |
| false || true | true |
Returns first truthy value |
| false || false | false |
Both falsy → returns last |
| 5 || "hello" | 5 |
First truthy → short-circuits |
| 0 || "hello" | "hello" |
First falsy, returns second |
| !true | false |
Inverts boolean |
| !0 | true |
0 is falsy, inverted to true |
| !!"hello" | true |
Convert to boolean: truthy |
Example: Logical operators and short-circuit evaluation
// Logical AND (&&) - returns first falsy or last value
console.log(true && true); // true
console.log(true && false); // false
console.log(5 && 10); // 10 (both truthy, returns last)
console.log(0 && 10); // 0 (first falsy)
console.log(null && "hello"); // null (first falsy)
// Logical OR (||) - returns first truthy or last value
console.log(false || true); // true
console.log(false || false); // false (last value)
console.log(5 || 10); // 5 (first truthy)
console.log(0 || 10); // 10 (first falsy, returns second)
console.log(null || "default"); // "default" (first falsy)
// Logical NOT (!)
console.log(!true); // false
console.log(!false); // true
console.log(!0); // true (0 is falsy)
console.log(!""); // true ("" is falsy)
console.log(!"hello"); // false ("hello" is truthy)
// Double NOT (!!) - convert to boolean
console.log(!!"hello"); // true
console.log(!!0); // false
console.log(!!""); // false
console.log(!!{}); // true (objects are truthy)
// Short-circuit evaluation - practical uses
// 1. Default values (before ?? was added)
let username = input || "Guest"; // If input is falsy, use "Guest"
// 2. Conditional execution
isLoggedIn && showDashboard(); // Execute if truthy
hasError || showSuccessMessage(); // Execute if falsy
// 3. Guard clauses
function processUser(user) {
user && user.profile && console.log(user.profile.name);
// Only accesses nested property if all are truthy
}
// 4. Avoiding function calls
let result = expensiveCheck() || cachedValue;
// If expensiveCheck() is truthy, cachedValue never evaluated
// Short-circuit prevents errors
let obj = null;
let value = obj && obj.property; // undefined (doesn't throw)
// Without short-circuit: obj.property would throw error
// Combining operators
let access = isAdmin || (isPremium && hasPermission);
console.log(true || (false && error())); // true, error() not called
// Falsy values: false, 0, "", null, undefined, NaN
console.log(false || 0 || "" || null || undefined || NaN || "found");
// "found" (first truthy)
Note: Logical operators return the actual value (not just boolean). Short-circuit evaluation
stops at first definitive result, useful for default values and conditional execution.
4.5 Bitwise Operators and Binary Operations
| Operator | Name | Description | Example | Binary | Result |
|---|---|---|---|---|---|
| & | AND | 1 if both bits are 1 | 5 & 3 |
0101 & 0011 |
1 (0001) |
| | | OR | 1 if at least one bit is 1 | 5 | 3 |
0101 | 0011 |
7 (0111) |
| ^ | XOR | 1 if bits are different | 5 ^ 3 |
0101 ^ 0011 |
6 (0110) |
| ~ | NOT | Inverts all bits | ~5 |
~0101 |
-6 (two's complement) |
| << | Left Shift | Shifts bits left, fills 0s | 5 << 1 |
0101 → 1010 |
10 |
| >> | Sign-propagating Right Shift | Shifts bits right, preserves sign | 5 >> 1 |
0101 → 0010 |
2 |
| >>> | Zero-fill Right Shift | Shifts bits right, fills 0s | -5 >>> 1 |
Fills with 0s | Large positive number |
| Use Case | Operation | Example | Purpose |
|---|---|---|---|
| Check if even | n & 1 |
5 & 1 = 1 (odd), 4 & 1 = 0 (even) |
Fast even/odd check |
| Multiply by 2n | n << x |
5 << 2 = 20 (5 × 4) |
Fast multiplication |
| Divide by 2n | n >> x |
20 >> 2 = 5 (20 ÷ 4) |
Fast division |
| Toggle bit | n ^ (1 << i) |
Toggles i-th bit | Flags/permissions |
| Set bit | n | (1 << i) |
Sets i-th bit to 1 | Enable flag |
| Clear bit | n & ~(1 << i) |
Sets i-th bit to 0 | Disable flag |
| Check bit | (n >> i) & 1 |
Gets value of i-th bit | Test flag |
| Swap variables | a ^= b; b ^= a; a ^= b; |
Swap without temp variable | XOR swap trick |
Example: Bitwise operations
// Basic bitwise operations
console.log(5 & 3); // 1 (0101 & 0011 = 0001)
console.log(5 | 3); // 7 (0101 | 0011 = 0111)
console.log(5 ^ 3); // 6 (0101 ^ 0011 = 0110)
console.log(~5); // -6 (inverts bits, two's complement)
// Bit shifts
console.log(5 << 1); // 10 (0101 → 1010) multiply by 2
console.log(5 << 2); // 20 (0101 → 10100) multiply by 4
console.log(10 >> 1); // 5 (1010 → 0101) divide by 2
console.log(20 >> 2); // 5 (10100 → 0101) divide by 4
// Practical use cases
// 1. Check if number is even or odd
function isEven(n) {
return (n & 1) === 0;
}
console.log(isEven(4)); // true
console.log(isEven(5)); // false
// 2. Flags and permissions
const READ = 1; // 001
const WRITE = 2; // 010
const EXECUTE = 4; // 100
let permissions = 0;
permissions |= READ; // Add READ permission
permissions |= WRITE; // Add WRITE permission
console.log(permissions & READ); // Check if READ (non-zero = true)
console.log(permissions & EXECUTE); // Check if EXECUTE (0 = false)
permissions &= ~WRITE; // Remove WRITE permission
console.log(permissions); // 1 (only READ)
// 3. Fast integer conversion
console.log(~~3.14); // 3 (double NOT truncates decimals)
console.log(5.7 | 0); // 5 (OR with 0 truncates)
console.log(5.7 >> 0); // 5 (right shift by 0 truncates)
// 4. XOR swap (without temp variable)
let a = 5, b = 10;
a ^= b; // a = 5 ^ 10
b ^= a; // b = 10 ^ (5 ^ 10) = 5
a ^= b; // a = (5 ^ 10) ^ 5 = 10
console.log(a, b); // 10 5
// 5. Toggle boolean
let flag = true;
flag ^= 1; // false (1 ^ 1 = 0)
flag ^= 1; // true (0 ^ 1 = 1)
// 6. Set specific bit
let n = 0b0000; // Binary literal
n |= (1 << 2); // Set bit 2: 0b0100
console.log(n.toString(2)); // "100"
// 7. Clear specific bit
n &= ~(1 << 2); // Clear bit 2: 0b0000
console.log(n.toString(2)); // "0"
Note: Bitwise operators work on 32-bit integers. Common uses include flags, permissions, fast
math operations, and low-level data manipulation.
4.6 Optional Chaining (?.) and Nullish Coalescing (??)
| Operator | Syntax | Returns | Use Case |
|---|---|---|---|
| ?. ES2020 | obj?.prop |
undefined if obj is null/undefined, else obj.prop |
Safe property access |
| ?.[] | obj?.[expr] |
undefined if obj is null/undefined |
Safe dynamic property access |
| ?.() | func?.(args) |
undefined if func is null/undefined |
Safe function call |
| ?? ES2020 | a ?? b |
a if not null/undefined, else b |
Default values for null/undefined only |
| Feature | || (OR) | ?? (Nullish Coalescing) |
|---|---|---|
| Falsy values treated as missing | Yes (false, 0, "", null, undefined, NaN) | No (only null/undefined) |
0 || 100 |
100 |
0 |
"" || "default" |
"default" |
"" |
false || true |
true |
false |
null ?? 100 |
100 |
100 |
undefined ?? 100 |
100 |
100 |
| Use case | When any falsy should use default | When only null/undefined should use default |
Example: Optional chaining and nullish coalescing
// Optional Chaining (?.)
// Without optional chaining (verbose and error-prone)
let user = null;
// let name = user.profile.name; // TypeError: Cannot read property 'profile' of null
let name = user && user.profile && user.profile.name; // undefined
// With optional chaining (concise and safe)
name = user?.profile?.name; // undefined (no error)
const user2 = {
profile: {
name: "Alice",
address: {
city: "NYC"
}
}
};
console.log(user2?.profile?.name); // "Alice"
console.log(user2?.profile?.address?.city); // "NYC"
console.log(user2?.profile?.phone); // undefined (no error)
console.log(user2?.account?.balance); // undefined
// Optional chaining with arrays
const arr = null;
console.log(arr?.[0]); // undefined (safe)
console.log(user2?.tags?.[0]); // undefined
// Optional chaining with function calls
const obj = {
method: function() { return "called"; }
};
console.log(obj.method?.()); // "called"
console.log(obj.nonExistent?.()); // undefined (no error)
console.log(user?.getProfile?.()); // undefined
// Nullish Coalescing (??)
// Problem with || operator
let count = 0;
let value1 = count || 10; // 10 (0 is falsy, uses default)
let value2 = count ?? 10; // 0 (0 is not null/undefined)
let text = "";
let msg1 = text || "default"; // "default" ("" is falsy)
let msg2 = text ?? "default"; // "" (empty string is valid)
// Use ?? for actual null/undefined checks
let config = {
timeout: 0, // 0 is valid
enabled: false, // false is valid
name: "" // empty string is valid
};
let timeout = config.timeout ?? 3000; // 0 (not null/undefined)
let enabled = config.enabled ?? true; // false (not null/undefined)
let name = config.name ?? "Unnamed"; // "" (not null/undefined)
let missing = config.missing ?? "N/A"; // "N/A" (undefined)
// Combining ?. and ??
const data = {
user: null,
settings: { theme: "dark" }
};
let theme = data?.settings?.theme ?? "light"; // "dark"
let userName = data?.user?.name ?? "Guest"; // "Guest"
// Real-world example: API response
function getUserCity(response) {
return response?.data?.user?.address?.city ?? "Unknown";
}
console.log(getUserCity(null)); // "Unknown"
console.log(getUserCity({ data: null })); // "Unknown"
console.log(getUserCity({
data: { user: { address: { city: "NYC" } } }
})); // "NYC"
// Short-circuit evaluation
let expensiveFn = () => { console.log("Called"); return 42; };
let result = null?.property?.method?.(); // undefined (expensiveFn not called)
console.log(result); // undefined
Warning: Don't overuse
?. as it can hide bugs. Use it for genuinely optional
properties, not to mask errors in required data.
4.7 Logical Assignment Operators (||=, &&=, ??=)
| Operator | Name | Equivalent | Assigns When |
|---|---|---|---|
| ||= ES2021 | Logical OR Assignment | x || (x = y) |
x is falsy |
| &&= ES2021 | Logical AND Assignment | x && (x = y) |
x is truthy |
| ??= ES2021 | Nullish Assignment | x ?? (x = y) |
x is null or undefined |
| Operator | Behavior | Use Case | Example |
|---|---|---|---|
| ||= | Assigns if current value is falsy | Set default for any falsy value | x ||= 10 (assigns if x is falsy) |
| &&= | Assigns if current value is truthy | Update existing truthy value | x &&= 10 (assigns if x is truthy) |
| ??= | Assigns only if null/undefined | Set default only for null/undefined | x ??= 10 (assigns if x is null/undefined) |
Example: Logical assignment operators
// ||= Logical OR Assignment
// Assigns if left side is falsy
let a = 0;
a ||= 10; // a is 10 (0 is falsy)
let b = 5;
b ||= 10; // b is still 5 (5 is truthy)
let c = "";
c ||= "default"; // c is "default" ("" is falsy)
// &&= Logical AND Assignment
// Assigns if left side is truthy
let x = 5;
x &&= 10; // x is 10 (5 is truthy)
let y = 0;
y &&= 10; // y is still 0 (0 is falsy, no assignment)
let z = null;
z &&= 10; // z is still null (null is falsy)
// ??= Nullish Assignment
// Assigns only if null or undefined
let p = 0;
p ??= 10; // p is still 0 (0 is not null/undefined)
let q = null;
q ??= 10; // q is 10 (null)
let r = undefined;
r ??= 10; // r is 10 (undefined)
let s = false;
s ??= true; // s is still false (false is not null/undefined)
// Practical use cases
// 1. Object property defaults with ??=
const config = {
timeout: 0, // 0 is valid
retry: null // null should be replaced
};
config.timeout ??= 3000; // Still 0 (not null/undefined)
config.retry ??= 3; // Now 3 (was null)
config.newProp ??= "default"; // "default" (was undefined)
console.log(config);
// { timeout: 0, retry: 3, newProp: "default" }
// 2. Accumulation with &&=
let total = 100;
let applyDiscount = true;
total &&= total * 0.9; // Apply 10% discount if total exists
console.log(total); // 90
let nullTotal = null;
nullTotal &&= nullTotal * 0.9; // No operation (nullTotal is falsy)
console.log(nullTotal); // null (unchanged)
// 3. Cache pattern with ||=
let cache = {};
function getData(key) {
// Set cache if not already set
cache[key] ||= expensiveOperation(key);
return cache[key];
}
function expensiveOperation(key) {
console.log("Computing...");
return key * 2;
}
console.log(getData(5)); // "Computing..." then 10
console.log(getData(5)); // 10 (from cache, no computing)
// 4. Updating objects conditionally
const user = {
name: "Alice",
age: null,
status: undefined
};
user.name ||= "Anonymous"; // Still "Alice" (truthy)
user.age ??= 18; // Now 18 (was null)
user.status ??= "active"; // Now "active" (was undefined)
// 5. Short-circuit: right side not evaluated if condition not met
let count = 0;
let getValue = () => { count++; return 100; };
let x1 = 5;
x1 ||= getValue(); // getValue() NOT called (x1 is truthy)
console.log(count); // 0
let x2 = 0;
x2 ||= getValue(); // getValue() IS called (x2 is falsy)
console.log(count); // 1
Note: Logical assignment operators combine logical operations with assignment, providing
cleaner syntax and short-circuit evaluation (right side only evaluated when assignment occurs).
Section 4 Summary
- Arithmetic operators include +, -, *, /, %, ** (exponentiation);
+concatenates strings if either operand is string - Assignment operators support compound forms (+=, -=, *=, etc.) for concise update operations
- Use strict equality (===, !==) to avoid type coercion bugs; loose equality (==) performs type conversion
- Logical operators (&&, ||, !) short-circuit and return actual values, not just booleans; useful for defaults and guards
- Bitwise operators work on 32-bit integers; useful for flags, permissions, and fast math operations
- Optional chaining (?.) safely accesses nested properties; nullish coalescing (??) provides defaults only for null/undefined
- Logical assignment (||=, &&=, ??=) combines logic with assignment; short-circuits when assignment unnecessary
5. Control Flow and Loop Constructs
5.1 Conditional Statements (if, switch, ternary)
| Statement | Syntax | Use Case | Notes |
|---|---|---|---|
| if | if (condition) { } |
Execute block if condition is truthy | Most common conditional |
| if...else | if (cond) { } else { } |
Execute one of two blocks | Binary choice |
| if...else if...else | if (c1) { } else if (c2) { } else { } |
Multiple conditions, evaluated in order | First truthy wins |
| switch | switch(expr) { case val: break; } |
Multiple discrete values, strict equality (===) | Don't forget break |
| Ternary | condition ? trueVal : falseVal |
Inline conditional expression | Returns value, concise |
| Nested Ternary | c1 ? v1 : c2 ? v2 : v3 |
Multiple conditions inline | Can be hard to read |
| Feature | if/else | switch | Ternary |
|---|---|---|---|
| Returns Value | No (statements) | No (statements) | Yes (expression) |
| Multiple Conditions | ✓ Any expression | Single expression, multiple cases | One condition per ternary |
| Comparison Type | Any truthy/falsy | Strict equality (===) | Any truthy/falsy |
| Fall-through | N/A | ✓ Without break |
N/A |
| Best For | Complex conditions, ranges | Many discrete values | Simple inline conditions |
Example: Conditional statements
// if statement
let age = 20;
if (age >= 18) {
console.log("Adult");
}
// if...else
if (age >= 18) {
console.log("Adult");
} else {
console.log("Minor");
}
// if...else if...else
let score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else if (score >= 70) {
console.log("C");
} else {
console.log("F");
}
// switch statement
let day = "Monday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("Weekday");
break; // Important!
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Invalid day");
}
// switch with expressions (modern pattern)
let result = switch(status) { // Not yet in JS, but coming
case 200: "OK";
case 404: "Not Found";
default: "Unknown";
};
// Ternary operator
let message = age >= 18 ? "Adult" : "Minor";
console.log(message);
// Nested ternary
let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
// Ternary with side effects (avoid for readability)
let value = condition ? (count++, doSomething()) : doSomethingElse();
// Multiple ternaries (can be hard to read)
let type = age < 13 ? "child"
: age < 20 ? "teen"
: age < 65 ? "adult"
: "senior";
// Guard clauses (early return pattern)
function processUser(user) {
if (!user) return; // Guard clause
if (!user.isActive) return; // Guard clause
if (!user.hasPermission) return; // Guard clause
// Main logic here
console.log("Processing user...");
}
Warning: Always use
break in switch cases to prevent fall-through (unless
intentional). Nested ternaries can reduce readability; use if/else for complex logic.
5.2 Loop Statements (for, while, do-while)
| Loop Type | Syntax | When to Use | Characteristics |
|---|---|---|---|
| for | for (init; cond; update) { } |
Known iteration count | Initialization, condition, update in header |
| while | while (condition) { } |
Unknown iteration count, condition first | Tests condition before each iteration |
| do-while | do { } while (condition); |
At least one iteration guaranteed | Tests condition after each iteration |
| Feature | for | while | do-while |
|---|---|---|---|
| Minimum Iterations | 0 (can skip entirely) | 0 (can skip entirely) | 1 (always executes once) |
| Condition Check | Before each iteration | Before each iteration | After each iteration |
| Loop Variable Scope | Block-scoped if using let/const | Declared outside loop | Declared outside loop |
| Best For | Counter-based iteration | Condition-based iteration | Execute-then-check pattern |
Example: Classic loop statements
// for loop - most common for arrays/counters
for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
// Multiple variables in for loop
for (let i = 0, j = 10; i < 5; i++, j--) {
console.log(i, j); // (0,10), (1,9), (2,8), (3,7), (4,6)
}
// Infinite loop (condition always true)
// for (;;) {
// console.log("Forever");
// break; // Need break to exit
// }
// while loop - condition-based
let count = 0;
while (count < 5) {
console.log(count);
count++;
}
// while with complex condition
let items = [1, 2, 3, 4, 5];
let i = 0;
while (i < items.length && items[i] !== 3) {
console.log(items[i]);
i++;
}
// Reading input until valid
let input;
while (!input || input.trim() === "") {
// input = prompt("Enter value:"); // Browser example
input = "test"; // Simulated
}
// do-while - at least one iteration
let num = 0;
do {
console.log(num); // Executes once even though num < 1 is false
num++;
} while (num < 1);
// Menu loop pattern
let choice;
do {
// Display menu
console.log("1. Option 1");
console.log("2. Option 2");
console.log("3. Exit");
// choice = getUserInput();
choice = 3; // Simulated
} while (choice !== 3);
// Empty for loop (all work in condition)
let arr = [1, 2, 3];
for (let i = 0; i < arr.length && arr[i]++ < 10;);
console.log(arr); // [2, 3, 4]
// Nested loops
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
console.log(`${i}, ${j}`);
}
}
// Loop with multiple updates
for (let i = 0; i < 10; i += 2) {
console.log(i); // 0, 2, 4, 6, 8
}
// Reverse loop
for (let i = 5; i >= 0; i--) {
console.log(i); // 5, 4, 3, 2, 1, 0
}
Note: Use
for for counter-based iteration, while for condition-based,
and do-while when you need at least one iteration before checking the condition.
5.3 Enhanced Loops (for...in, for...of, for await...of)
| Loop Type | Syntax | Iterates Over | Returns | Use For |
|---|---|---|---|---|
| for...in | for (key in obj) { } |
Enumerable properties | Property names (strings) | Object properties |
| for...of ES6 | for (val of iterable) { } |
Iterable objects | Values | Arrays, strings, Maps, Sets |
| for await...of ES2018 | for await (val of asyncIterable) { } |
Async iterables | Resolved promises | Async generators, streams |
| Feature | for...in | for...of |
|---|---|---|
| Works with Objects | ✓ Yes (enumerable properties) | ✗ No (unless iterable) |
| Works with Arrays | ✓ Yes (indices as strings) | ✓ Yes (values directly) |
| Works with Strings | ✓ Yes (indices) | ✓ Yes (characters) |
| Works with Map/Set | ✗ No | ✓ Yes |
| Includes Prototype Chain | ✓ Yes (inherited properties) | ✗ No |
| Recommended For | Object properties (use with hasOwnProperty) |
Iterables (arrays, strings, collections) |
Example: Enhanced loops
// for...in - iterates over object properties
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
console.log(key, obj[key]); // "a" 1, "b" 2, "c" 3
}
// for...in with arrays (NOT recommended - use for...of)
const arr = [10, 20, 30];
for (let index in arr) {
console.log(index, arr[index]); // "0" 10, "1" 20, "2" 30
console.log(typeof index); // "string" (not number!)
}
// for...in includes inherited properties
Object.prototype.inheritedProp = "inherited";
const obj2 = { own: "value" };
for (let key in obj2) {
console.log(key); // "own", "inheritedProp"
}
// Use hasOwnProperty to filter
for (let key in obj2) {
if (obj2.hasOwnProperty(key)) {
console.log(key); // "own" only
}
}
// for...of - iterates over iterable values (ES6)
const arr2 = [10, 20, 30];
for (let value of arr2) {
console.log(value); // 10, 20, 30 (values, not indices)
}
// for...of with strings
const str = "hello";
for (let char of str) {
console.log(char); // "h", "e", "l", "l", "o"
}
// for...of with Map
const map = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of map) {
console.log(key, value); // "a" 1, "b" 2
}
// for...of with Set
const set = new Set([1, 2, 3]);
for (let value of set) {
console.log(value); // 1, 2, 3
}
// for...of with array methods
const arr3 = [1, 2, 3, 4, 5];
for (let value of arr3.entries()) {
console.log(value); // [0, 1], [1, 2], [2, 3]...
}
for (let [index, value] of arr3.entries()) {
console.log(index, value); // 0 1, 1 2, 2 3...
}
// for await...of - async iteration (ES2018)
async function fetchSequentially() {
const urls = ['url1', 'url2', 'url3'];
// Create async iterable
async function* fetchUrls() {
for (let url of urls) {
yield fetch(url); // Simulated
}
}
// Iterate and wait for each promise
for await (let response of fetchUrls()) {
console.log(await response.text());
}
}
// for await...of with Promise array
async function processPromises() {
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
for await (let value of promises) {
console.log(value); // 1, 2, 3 (waits for each)
}
}
// Custom iterable with for...of
const customIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index],
done: index++ >= this.data.length
})
};
}
};
for (let value of customIterable) {
console.log(value); // 1, 2, 3
}
Warning: Use
for...of for arrays (not for...in). With
for...in on objects, always use hasOwnProperty() to avoid inherited properties.
5.4 Jump Statements (break, continue, return)
| Statement | Syntax | Effect | Use In |
|---|---|---|---|
| break | break; |
Exits loop or switch immediately | Loops, switch |
| break label | break labelName; |
Exits named loop/block | Nested loops |
| continue | continue; |
Skips to next iteration | Loops only |
| continue label | continue labelName; |
Continues named loop | Nested loops |
| return | return value; |
Exits function with value | Functions only |
| return (void) | return; |
Exits function, returns undefined |
Functions only |
| Statement | break | continue | return |
|---|---|---|---|
| In for loop | ✓ Exits loop | ✓ Next iteration | ✓ Exits function (and loop) |
| In while loop | ✓ Exits loop | ✓ Next iteration | ✓ Exits function (and loop) |
| In switch | ✓ Exits switch | ✗ Syntax error | ✓ Exits function (and switch) |
| In function | ✗ Must be in loop/switch | ✗ Must be in loop | ✓ Exits function |
| With labels | ✓ Can target outer loop | ✓ Can target outer loop | ✗ Cannot use labels |
Example: Jump statements
// break - exit loop immediately
for (let i = 0; i < 10; i++) {
if (i === 5) break; // Exit loop when i is 5
console.log(i); // 0, 1, 2, 3, 4
}
// break in nested loop (only exits inner loop)
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) break; // Exits inner loop only
console.log(`${i},${j}`);
}
}
// Output: 0,0 1,0 2,0
// continue - skip to next iteration
for (let i = 0; i < 5; i++) {
if (i === 2) continue; // Skip when i is 2
console.log(i); // 0, 1, 3, 4
}
// continue with condition
const arr = [1, 2, 3, 4, 5];
for (let num of arr) {
if (num % 2 === 0) continue; // Skip even numbers
console.log(num); // 1, 3, 5
}
// return - exit function
function findValue(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i; // Exit function, return index
}
}
return -1; // Not found
}
console.log(findValue([1, 2, 3], 2)); // 1
console.log(findValue([1, 2, 3], 5)); // -1
// return without value (returns undefined)
function logMessage(msg) {
if (!msg) return; // Early exit
console.log(msg);
}
// Multiple return points (guard clauses)
function validateUser(user) {
if (!user) return { valid: false, error: "No user" };
if (!user.name) return { valid: false, error: "No name" };
if (!user.email) return { valid: false, error: "No email" };
return { valid: true };
}
// break in switch
function getDayType(day) {
switch (day) {
case 'Mon':
case 'Tue':
case 'Wed':
case 'Thu':
case 'Fri':
return 'Weekday';
// No break needed after return
case 'Sat':
case 'Sun':
return 'Weekend';
default:
return 'Invalid';
}
}
// Finding first match
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
];
let found;
for (let user of users) {
if (user.age > 28) {
found = user;
break; // Stop after finding first match
}
}
console.log(found); // Bob
// Skip invalid items
const data = [1, null, 2, undefined, 3, NaN, 4];
for (let item of data) {
if (item == null || Number.isNaN(item)) continue;
console.log(item); // 1, 2, 3, 4
}
Note: Use
break to exit loops early, continue to skip iterations, and
return to exit functions. For nested loops, consider labeled statements for more control.
5.5 Exception Handling (try, catch, finally, throw)
| Keyword | Purpose | Required | Description |
|---|---|---|---|
| try | Execute code that may throw | ✓ Yes | Contains code to be tested for errors |
| catch | Handle exceptions | ✓ Yes (or finally) | Executes if error thrown in try block |
| finally | Cleanup code | ✗ Optional | Always executes (even if error/return) |
| throw | Throw exception | N/A | Creates and throws error object |
| Error Type | When Thrown | Example |
|---|---|---|
| Error | Generic error (base class) | new Error("message") |
| SyntaxError | Invalid JavaScript syntax | eval("{") |
| ReferenceError | Invalid reference/undeclared variable | console.log(undeclaredVar) |
| TypeError | Value not of expected type | null.property |
| RangeError | Value not in allowed range | new Array(-1) |
| URIError | Invalid URI encoding/decoding | decodeURI("%") |
Example: Exception handling
// Basic try-catch
try {
let result = riskyOperation();
console.log(result);
} catch (error) {
console.error("Error:", error.message);
}
// Accessing error properties
try {
throw new Error("Something went wrong");
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace
}
// try-catch-finally
try {
console.log("Try block");
// throw new Error("Test");
} catch (error) {
console.log("Catch block:", error.message);
} finally {
console.log("Finally always runs");
}
// finally runs even with return
function testFinally() {
try {
return "from try";
} catch (error) {
return "from catch";
} finally {
console.log("Finally executed"); // Runs before return
}
}
console.log(testFinally()); // "Finally executed", then "from try"
// Throwing errors
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
try {
console.log(divide(10, 0));
} catch (error) {
console.log("Caught:", error.message);
}
// Throwing custom errors
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateAge(age) {
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
if (age > 150) {
throw new ValidationError("Age too high");
}
return true;
}
try {
validateAge(-5);
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation error:", error.message);
} else {
console.log("Other error:", error);
}
}
// Nested try-catch
try {
try {
throw new Error("Inner error");
} catch (innerError) {
console.log("Inner catch:", innerError.message);
throw new Error("Outer error"); // Re-throw or new error
}
} catch (outerError) {
console.log("Outer catch:", outerError.message);
}
// Catching specific error types
try {
// Some operation
JSON.parse("invalid json");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("JSON syntax error");
} else if (error instanceof TypeError) {
console.log("Type error");
} else {
console.log("Unknown error");
}
}
// Async error handling
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch error:", error);
throw error; // Re-throw for caller to handle
} finally {
console.log("Cleanup operations");
}
}
// Error without catch (just finally)
try {
console.log("Executing...");
} finally {
console.log("Cleanup"); // Valid: try-finally without catch
}
// Throwing non-Error objects (possible but not recommended)
try {
throw "String error"; // Works but not best practice
throw { code: 500 }; // Works but no stack trace
throw 42; // Works but no context
} catch (error) {
console.log(typeof error); // Could be anything
}
Warning: Always catch errors for operations that may fail (network, parsing, etc.). Use
finally for cleanup that must run regardless of success/failure.
5.6 Labels and Labeled Statements
| Feature | Syntax | Use With | Purpose |
|---|---|---|---|
| Label Definition | labelName: statement |
Any statement/block | Names a statement for reference |
| Labeled break | break labelName; |
Loops, blocks | Exits named loop/block |
| Labeled continue | continue labelName; |
Loops only | Continues named loop |
| Use Case | Without Labels | With Labels |
|---|---|---|
| Break from nested loop | Only breaks inner loop | Can break outer loop directly |
| Continue outer loop | Requires flag variable | Direct continue to outer loop |
| Exit multiple levels | Multiple break statements | Single labeled break |
| Code clarity | Complex control flow | Explicit jump targets |
Example: Labeled statements
// Labeled break - exit outer loop
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outer; // Breaks out of outer loop
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 0,1 0,2 1,0
// Without label (only breaks inner loop)
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break; // Only breaks inner loop
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 0,1 0,2 1,0 2,0 2,1 2,2
// Labeled continue - continue outer loop
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) {
continue outer; // Continues outer loop, skips rest of inner
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 1,0 2,0
// Finding element in 2D array
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
let target = 5;
let found = false;
search: for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === target) {
console.log(`Found at [${i}][${j}]`);
found = true;
break search; // Exit both loops
}
}
}
// Multiple nested loops with labels
outer: for (let i = 0; i < 2; i++) {
middle: for (let j = 0; j < 2; j++) {
inner: for (let k = 0; k < 2; k++) {
if (i === 1) break outer; // Exit all loops
if (j === 1) break middle; // Exit middle and inner
if (k === 1) break inner; // Exit only inner
console.log(`${i},${j},${k}`);
}
}
}
// Labeled block (not loop)
blockLabel: {
console.log("Start");
if (someCondition) {
break blockLabel; // Exit block early
}
console.log("This may not execute");
}
console.log("After block");
// Complex search with labeled break
const data = [
{ id: 1, items: [10, 20, 30] },
{ id: 2, items: [40, 50, 60] },
{ id: 3, items: [70, 80, 90] }
];
let searchValue = 50;
let result = null;
dataSearch: for (let group of data) {
for (let item of group.items) {
if (item === searchValue) {
result = { groupId: group.id, value: item };
break dataSearch; // Found, exit all loops
}
}
}
console.log(result); // { groupId: 2, value: 50 }
// Alternative to labeled break (using function return)
function findInMatrix(matrix, target) {
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === target) {
return [i, j]; // Return exits function (simpler)
}
}
}
return null;
}
// Labeled continue with condition
outer: for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (i + j > 5) {
continue outer; // Skip to next outer iteration
}
console.log(`Sum: ${i + j}`);
}
}
Note: Labeled statements are useful for complex nested loops but can reduce readability.
Consider refactoring into separate functions with
return as a cleaner alternative.
Section 5 Summary
- Conditionals: Use
if/elsefor complex logic,switchfor discrete values, ternary for inline expressions - Loops:
for(known count),while(condition-based),do-while(at least once) - Enhanced loops:
for...offor iterables (arrays, strings),for...infor object properties,for await...offor async - Jump statements:
breakexits loops/switch,continueskips to next iteration,returnexits functions - Exception handling:
try-catch-finallyfor error handling;throwto raise errors;finallyalways executes - Labels: Enable break/continue to target outer loops; useful for nested structures but consider function refactoring
6. Functions and Functional Programming
6.1 Function Declaration and Expression Syntax
| Function Type | Syntax | Hoisting | Use Case |
|---|---|---|---|
| Function Declaration | function name(params) { body } |
✓ Fully hoisted (can call before declaration) | Top-level functions, utility functions requiring hoisting |
| Function Expression | const name = function(params) { body }; |
✗ Variable hoisted but undefined (TDZ for let/const) | Conditional function creation, callbacks, function as data |
| Named Function Expression | const f = function name(params) { body }; |
✗ Not hoisted; name only visible inside function | Recursion in expressions, better stack traces |
| Arrow Function ES6 | const name = (params) => expression |
✗ Variable hoisting behavior | Short callbacks, lexical this binding, concise syntax |
| Method Definition ES6 | { methodName(params) { body } } |
✗ Part of object/class definition | Object methods, class methods with concise syntax |
| Generator Function ES6 | function* name(params) { yield value } |
✓ Hoisted like regular functions | Iterators, lazy evaluation, pausable execution |
| Async Function ES8 | async function name(params) { await promise } |
✓ Hoisted (declaration) / ✗ (expression) | Asynchronous operations with cleaner syntax |
Example: Function declaration vs expression
// Function Declaration - hoisted, can call before definition
console.log(add(2, 3)); // Works: 5
function add(a, b) {
return a + b;
}
// Function Expression - not hoisted
// console.log(subtract(5, 2)); // Error: Cannot access before initialization
const subtract = function(a, b) {
return a - b;
};
// Named Function Expression - name visible only inside
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1); // 'fact' accessible here
};
console.log(factorial(5)); // 120
// console.log(fact(5)); // Error: fact is not defined
// Arrow function - concise syntax
const multiply = (a, b) => a * b;
const square = x => x * x; // Single param, no parens needed
const greet = () => 'Hello'; // No params
Best Practice: Use function declarations for top-level utility functions that need hoisting.
Use arrow functions for callbacks and methods that don't need their own
this. Use function
expressions when functions are conditional or need to be treated as values.
6.2 Arrow Functions and Lexical this Binding
| Feature | Arrow Function | Regular Function | Impact |
|---|---|---|---|
| this Binding | Lexical (from enclosing scope) | Dynamic (based on call) | No .bind() needed for callbacks |
| arguments Object | ✗ Not available (use rest params) | ✓ Available | Use ...args for arrow functions |
| new Operator | ✗ Cannot be constructor | ✓ Can be constructor | Arrow functions have no [[Construct]] |
| prototype Property | ✗ No prototype | ✓ Has prototype | Lighter memory footprint for arrows |
| super Keyword | ✗ Cannot use super | ✓ Can use in methods | Use regular functions for class methods needing super |
| yield Keyword | ✗ Cannot be generator | ✓ Can yield | Use function* for generators |
| Syntax Form | Example | When to Use |
|---|---|---|
| No Parameters | () => expression |
Functions with no inputs |
| Single Parameter | x => expression |
Unary functions (map, filter callbacks) |
| Multiple Parameters | (x, y) => expression |
Binary/multi-parameter functions |
| Expression Body | x => x * 2 |
Single expression, implicit return |
| Block Body | x => { return x * 2; } |
Multiple statements, explicit return |
| Object Literal Return | x => ({ value: x }) |
Returning object (wrap in parentheses) |
Example: Lexical this binding in practice
// Problem with regular functions - this binding changes
function Timer() {
this.seconds = 0;
// Regular function - 'this' is window/undefined in strict mode
setInterval(function() {
this.seconds++; // ✗ Wrong 'this'
console.log(this.seconds); // NaN
}, 1000);
}
// Solution 1: Arrow function - lexical 'this'
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // ✓ Correct 'this' from Timer
console.log(this.seconds); // 1, 2, 3...
}, 1000);
}
// Solution 2: Old approach with .bind() (before ES6)
function Timer() {
this.seconds = 0;
setInterval(function() {
this.seconds++;
}.bind(this), 1000); // Explicitly bind 'this'
}
// Class methods with arrow functions
class Counter {
count = 0;
// Arrow function as class field - 'this' always bound to instance
increment = () => {
this.count++;
}
// Regular method - 'this' depends on how it's called
decrement() {
this.count--;
}
}
const counter = new Counter();
const inc = counter.increment;
const dec = counter.decrement;
inc(); // ✓ Works - arrow function has lexical 'this'
dec(); // ✗ Error - 'this' is undefined (not bound)
Example: Arrow function syntax variations
// Expression body - implicit return
const double = x => x * 2;
const sum = (a, b) => a + b;
// Block body - explicit return needed
const greet = name => {
const message = `Hello, ${name}!`;
return message.toUpperCase();
};
// Returning object literal - wrap in parentheses
const makePerson = (name, age) => ({ name, age });
const point = (x, y) => ({ x: x, y: y });
// Array methods with arrows
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
// Chaining with concise arrows
const result = [1, 2, 3, 4, 5]
.filter(x => x % 2 === 0)
.map(x => x * x)
.reduce((sum, x) => sum + x, 0); // 20
Warning: Don't use arrow functions as object methods or event handlers where you need dynamic
this. Arrow functions cannot be used as constructors with new. They don't have
arguments object - use rest parameters instead.
6.3 Function Parameters (default, rest, destructuring)
| Parameter Type | Syntax | Description | Example |
|---|---|---|---|
| Required Parameters | function f(a, b) |
Traditional positional parameters; undefined if not provided | f(1, 2) |
| Default Parameters ES6 | function f(a = 0, b = 1) |
Provides default value if undefined (not for null) | f() // a=0, b=1 |
| Rest Parameters ES6 | function f(...args) |
Collects remaining arguments into array; must be last | f(1,2,3) // args=[1,2,3] |
| Destructuring (Array) ES6 | function f([a, b]) |
Extracts values from array argument by position | f([10, 20]) |
| Destructuring (Object) ES6 | function f({x, y}) |
Extracts properties from object argument by name | f({x:1, y:2}) |
| Nested Destructuring ES6 | function f({a: {b}}) |
Extracts nested properties from deep object structure | f({a: {b: 5}}) |
| Default + Destructuring ES6 | function f({x = 0} = {}) |
Combines destructuring with defaults for object properties | f() // x=0 |
| Pattern | Example | Behavior |
|---|---|---|
| Default with Expression | f(a = computeDefault()) |
Expression evaluated only if parameter is undefined |
| Using Previous Params | f(a, b = a * 2) |
Default can reference earlier parameters |
| Rest with Destructuring | f([first, ...rest]) |
Combine array destructuring with rest collection |
| Rename in Destructuring | f({x: width, y: height}) |
Extract and rename properties simultaneously |
Example: Default parameters
// Basic default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Bob', 'Hi')); // "Hi, Bob!"
// Default only applies to undefined, not null
console.log(greet(null)); // "Hello, null!" (null !== undefined)
console.log(greet(undefined, 'Hey')); // "Hey, Guest!" (undefined triggers default)
// Default expressions evaluated at call time
let counter = 0;
function f(x = counter++) {
return x;
}
console.log(f()); // 0 (counter becomes 1)
console.log(f()); // 1 (counter becomes 2)
console.log(f(10)); // 10 (default not used, counter stays 2)
// Using previous parameters in defaults
function createRect(width, height = width) {
return { width, height }; // height defaults to width (square)
}
console.log(createRect(5)); // { width: 5, height: 5 }
console.log(createRect(5, 10)); // { width: 5, height: 10 }
Example: Rest parameters
// Rest parameters collect remaining arguments into array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum()); // 0 (empty array)
// Combine regular params with rest
function multiply(multiplier, ...numbers) {
return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
// Rest must be last parameter
// function invalid(...args, last) {} // ✗ SyntaxError
// Rest vs arguments object
function oldWay() {
// arguments is array-like, not real array
const args = Array.from(arguments);
return args.reduce((sum, n) => sum + n, 0);
}
function newWay(...args) {
// args is real array with all array methods
return args.reduce((sum, n) => sum + n, 0);
}
// Arrow functions don't have arguments, use rest
const arrowSum = (...nums) => nums.reduce((s, n) => s + n, 0);
Example: Destructuring parameters
// Object destructuring in parameters
function createUser({ name, age, email = 'none@example.com' }) {
return { name, age, email };
}
createUser({ name: 'Alice', age: 30 });
// { name: 'Alice', age: 30, email: 'none@example.com' }
// Array destructuring in parameters
function sumFirstTwo([a, b]) {
return a + b;
}
console.log(sumFirstTwo([10, 20, 30])); // 30
// Nested destructuring
function displayPoint({ coords: { x, y }, label = 'Point' }) {
return `${label}: (${x}, ${y})`;
}
displayPoint({ coords: { x: 10, y: 20 } }); // "Point: (10, 20)"
// Destructuring with rest
function getFirstAndRest([first, ...rest]) {
return { first, rest };
}
console.log(getFirstAndRest([1, 2, 3, 4]));
// { first: 1, rest: [2, 3, 4] }
// Rename while destructuring
function printDimensions({ width: w, height: h }) {
console.log(`Width: ${w}, Height: ${h}`);
}
printDimensions({ width: 100, height: 200 });
// Default for entire destructured object
function config({ host = 'localhost', port = 8080 } = {}) {
return `${host}:${port}`;
}
console.log(config()); // "localhost:8080"
console.log(config({ host: 'example.com' })); // "example.com:8080"
6.4 Higher-Order Functions and Callbacks
| Concept | Definition | Common Use Cases |
|---|---|---|
| Higher-Order Function (HOF) | Function that takes function(s) as argument(s) and/or returns a function | map, filter, reduce, addEventListener, middleware |
| Callback Function | Function passed as argument to be executed later (synchronous or asynchronous) | Event handlers, async operations, array methods |
| Function Returning Function | HOF that creates and returns new function (function factory) | Configuration, currying, closures, partial application |
| Function Composition | Combining functions where output of one is input to another | Data transformation pipelines, functional programming |
| Built-in HOF | Purpose | Callback Signature | Returns |
|---|---|---|---|
| Array.map() | Transform each element | (element, index, array) => newValue |
New array (same length) |
| Array.filter() | Select elements by condition | (element, index, array) => boolean |
New array (≤ original length) |
| Array.reduce() | Aggregate to single value | (accumulator, element, index, array) => newAcc |
Single accumulated value |
| Array.forEach() | Execute side effects | (element, index, array) => void |
undefined (no return value) |
| Array.find() | Find first matching element | (element, index, array) => boolean |
Element or undefined |
| Array.some() | Test if any element passes | (element, index, array) => boolean |
boolean |
| Array.every() | Test if all elements pass | (element, index, array) => boolean |
boolean |
Example: Higher-order functions
// Function that takes function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // Logs: 0, 1, 2
// Function that returns function (function factory)
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Function composition
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
const addOne = x => x + 1;
const square = x => x * x;
const addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(4)); // 25 (5²)
// Practical HOF: logger wrapper
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // Logs call info, returns 5
Example: Array HOF methods in action
const users = [
{ id: 1, name: 'Alice', age: 25, active: true },
{ id: 2, name: 'Bob', age: 30, active: false },
{ id: 3, name: 'Charlie', age: 35, active: true }
];
// map - transform data
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']
// filter - select subset
const activeUsers = users.filter(user => user.active);
// [Alice, Charlie]
// reduce - aggregate
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90
// Chaining HOFs
const result = users
.filter(user => user.active)
.map(user => user.name)
.map(name => name.toUpperCase());
// ['ALICE', 'CHARLIE']
// find - first match
const user = users.find(u => u.age > 25);
// { id: 2, name: 'Bob', age: 30, active: false }
// some - any match
const hasInactive = users.some(u => !u.active); // true
// every - all match
const allActive = users.every(u => u.active); // false
// Complex reduce example
const grouped = users.reduce((groups, user) => {
const key = user.active ? 'active' : 'inactive';
groups[key] = groups[key] || [];
groups[key].push(user);
return groups;
}, {});
// { active: [Alice, Charlie], inactive: [Bob] }
Best Practice: Prefer declarative HOFs (map, filter, reduce) over imperative loops for data
transformation. Chain operations for readability. Use arrow functions for concise callbacks. Remember that HOFs
enable code reuse and abstraction.
6.5 Closures and Function Scope Management
| Concept | Definition | Key Characteristics |
|---|---|---|
| Closure | Function that retains access to variables from its outer (enclosing) scope even after outer function has returned | Lexical scoping, persistent state, data privacy |
| Lexical Environment | Internal structure holding variable bindings and reference to outer environment | Created at function execution, forms scope chain |
| Scope Chain | Chain of lexical environments searched for variable resolution | Inner to outer traversal, stops at first match |
| Private Variables | Variables accessible only through closures, not directly from outside | Encapsulation, data hiding, module pattern |
| Closure Pattern | Use Case | Example Pattern |
|---|---|---|
| Function Factory | Create specialized functions with preset parameters | function makeAdder(x) { return y => x + y; } |
| Private State | Encapsulate data with controlled access | function counter() { let n=0; return ()=>++n; } |
| Module Pattern | Create modules with public/private members | IIFE returning object with methods accessing private vars |
| Event Handlers | Preserve context in async callbacks | Handler functions accessing outer scope variables |
| Memoization | Cache function results using closure-stored cache | Closure over cache object for recursive functions |
Example: Basic closure mechanics
// Simple closure - inner function accesses outer variable
function outer() {
let count = 0; // Variable in outer scope
function inner() {
count++; // Accesses count from outer scope
return count;
}
return inner;
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls through closure
// Multiple closures over same variable
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const c1 = makeCounter();
const c2 = makeCounter(); // Separate closure, separate count
c1.increment();
c1.increment();
console.log(c1.value()); // 2
console.log(c2.value()); // 0 (independent)
Example: Practical closure patterns
// Function factory with closure
function multiplier(factor) {
return function(number) {
return number * factor; // 'factor' remembered via closure
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Private variables pattern
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
// No direct access to 'balance' from outside
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
console.log(account.balance); // undefined (private)
console.log(account.getBalance()); // 120
// Memoization with closure
function memoize(fn) {
const cache = {}; // Closure over cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('Cached');
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const slowFunction = (n) => {
console.log('Computing...');
return n * 2;
};
const fast = memoize(slowFunction);
fast(5); // Computing... 10
fast(5); // Cached 10
Common Pitfall: Closures in loops. Using
var creates single binding shared across
iterations. Solution: use let (creates new binding per iteration) or IIFE to capture value.
// ✗ Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all closures see same 'i')
// ✓ Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 (each closure gets own 'i')
// ✓ Solution 2: IIFE (old approach)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Logs: 0, 1, 2
6.6 IIFE Patterns and Module Creation
| Pattern | Syntax | Purpose | Use Case |
|---|---|---|---|
| Basic IIFE | (function() { })(); |
Execute function immediately, create private scope | Avoid global pollution, one-time initialization |
| Alternative Syntax | (function() { }()); |
Same as basic IIFE (different grouping) | Style preference, same behavior |
| Arrow IIFE ES6 | (() => { })(); |
Concise IIFE with lexical this | Modern alternative, shorter syntax |
| Named IIFE | (function name() { })(); |
Self-reference for recursion, better debugging | Recursive IIFEs, stack traces |
| IIFE with Parameters | (function(x) { })(value); |
Pass values into IIFE scope | Dependency injection, value capture |
| IIFE Returning Value | const x = (function() { return val; })(); |
Initialize variable with complex logic | Complex initialization, module pattern |
| Module Pattern (IIFE) | const mod = (function() { return {...}; })(); |
Create module with public API, private state | Pre-ES6 modules, encapsulation |
Example: IIFE basics and variations
// Basic IIFE - executes immediately
(function() {
const message = 'Hello from IIFE';
console.log(message);
})(); // Logs: Hello from IIFE
// 'message' not accessible here (private scope)
// IIFE with parameters
(function(name) {
console.log(`Hello, ${name}!`);
})('Alice'); // Hello, Alice!
// IIFE returning value
const config = (function() {
const privateKey = 'secret123';
return {
apiUrl: 'https://api.example.com',
timeout: 5000,
getKey: () => privateKey // Controlled access
};
})();
console.log(config.apiUrl); // 'https://api.example.com'
console.log(config.privateKey); // undefined (private)
// Arrow function IIFE (ES6+)
(() => {
const temp = 'temporary variable';
console.log(temp);
})();
// Named IIFE for recursion
(function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
})(5); // 120
// IIFE for variable capture in loops (pre-let solution)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => console.log(index), 100);
})(i); // Capture current value of i
}
Example: Module pattern with IIFE
// Classic Module Pattern
const Calculator = (function() {
// Private variables and functions
let history = [];
function log(operation, result) {
history.push({ operation, result, timestamp: Date.now() });
}
// Public API
return {
add(a, b) {
const result = a + b;
log('add', result);
return result;
},
subtract(a, b) {
const result = a - b;
log('subtract', result);
return result;
},
getHistory() {
return [...history]; // Return copy, not reference
},
clearHistory() {
history = [];
}
};
})();
Calculator.add(5, 3); // 8
Calculator.subtract(10, 4); // 6
console.log(Calculator.getHistory()); // Array of operations
console.log(Calculator.history); // undefined (private)
// Revealing Module Pattern
const Counter = (function() {
let count = 0;
function increment() { return ++count; }
function decrement() { return --count; }
function getValue() { return count; }
function reset() { count = 0; }
// Reveal selected functions
return {
increment,
decrement,
getValue,
reset
};
})();
Counter.increment(); // 1
Counter.increment(); // 2
console.log(Counter.getValue()); // 2
// Module with dependencies
const UserManager = (function($, _) {
// Use jQuery ($) and lodash (_) passed as parameters
function getUsers() {
return _.map(users, user => user.name);
}
return { getUsers };
})(jQuery, lodash); // Inject dependencies
Modern Alternative: ES6 modules (
import/export) have largely replaced IIFE-based
modules. However, IIFEs remain useful for: creating private scope, avoiding global pollution, one-time
initialization code, and browser compatibility when modules aren't available.
6.7 Function Methods (call, apply, bind)
| Method | Syntax | Arguments | Returns | When Called |
|---|---|---|---|---|
| call() | fn.call(thisArg, arg1, arg2, ...) |
Individual arguments | Function result | Immediately |
| apply() | fn.apply(thisArg, [args]) |
Array of arguments | Function result | Immediately |
| bind() | fn.bind(thisArg, arg1, ...) |
Individual arguments (partial) | New bound function | Later (returned fn) |
| Use Case | Best Method | Why |
|---|---|---|
| Set 'this' and call now | call() | Known number of arguments, immediate execution |
| Set 'this' with array args | apply() | Arguments already in array, or use Math.max/min with arrays |
| Set 'this' for later use | bind() | Event handlers, callbacks, partial application |
| Partial application | bind() | Pre-fill some arguments, returns reusable function |
| Borrowing methods | call() or apply() | Use array methods on array-like objects |
Example: call() and apply()
const person = {
name: 'Alice',
greet(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
};
console.log(person.greet('Hello', '!')); // "Hello, I'm Alice!"
// call() - set 'this' and pass individual arguments
const otherPerson = { name: 'Bob' };
console.log(person.greet.call(otherPerson, 'Hi', '.'));
// "Hi, I'm Bob."
// apply() - set 'this' and pass arguments as array
console.log(person.greet.apply(otherPerson, ['Hey', '...']));
// "Hey, I'm Bob..."
// Practical: borrowing array methods for array-like objects
function sumAll() {
// 'arguments' is array-like, not real array
// Borrow reduce() from Array
return Array.prototype.reduce.call(arguments, (sum, n) => sum + n, 0);
}
console.log(sumAll(1, 2, 3, 4)); // 10
// Math.max with apply (pre-spread operator)
const numbers = [5, 2, 9, 1, 7];
const max = Math.max.apply(null, numbers); // 9
// Modern: Math.max(...numbers)
// Invoking function with specific context
function introduce() {
return `My name is ${this.name} and I'm ${this.age}`;
}
const user = { name: 'Charlie', age: 30 };
console.log(introduce.call(user));
// "My name is Charlie and I'm 30"
Example: bind() for permanent binding
const obj = {
value: 42,
getValue() {
return this.value;
}
};
// Problem: 'this' lost when method assigned to variable
const getValue = obj.getValue;
console.log(getValue()); // undefined ('this' is window/undefined)
// Solution: bind() creates new function with fixed 'this'
const boundGetValue = obj.getValue.bind(obj);
console.log(boundGetValue()); // 42
// Practical: event handlers
class Button {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
handleClick() {
this.clickCount++;
console.log(`${this.label} clicked ${this.clickCount} times`);
}
}
const btn = new Button('Submit');
// Without bind, 'this' would be the button element, not our object
document.querySelector('button').addEventListener('click',
btn.handleClick.bind(btn)
);
// Partial application with bind()
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // Pre-fill first argument
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Binding with setTimeout
const timer = {
seconds: 0,
start() {
// bind() ensures 'this' refers to timer object
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
// Modern alternative: arrow function (lexical 'this')
// setInterval(() => { this.seconds++; }, 1000);
}
};
// Multiple bind() calls - first one wins
function log() { console.log(this.value); }
const obj1 = { value: 1 };
const obj2 = { value: 2 };
const bound1 = log.bind(obj1);
const bound2 = bound1.bind(obj2); // Has no effect
bound2(); // 1 (not 2 - can't rebind)
Modern Context: Arrow functions (
=>) with lexical this have
reduced the need for bind() in many cases. However, call(), apply(), and
bind() remain essential for: method borrowing, explicit context control, partial application, and
working with legacy code.
6.8 Recursion and Tail Call Optimization
| Concept | Definition | Characteristics |
|---|---|---|
| Recursion | Function calling itself to solve smaller subproblems | Base case + recursive case; elegant for tree/graph problems |
| Base Case | Condition that stops recursion without further calls | Prevents infinite recursion; returns direct result |
| Recursive Case | Logic that breaks problem down and calls function recursively | Progress toward base case; must modify parameters |
| Call Stack | Stack storing execution contexts for function calls | Each recursive call adds frame; limited size (stack overflow risk) |
| Tail Call | Function call that's the last operation (nothing after return) | Eligible for optimization; no pending operations |
| Tail Call Optimization (TCO) LIMITED | Reuse stack frame for tail calls instead of creating new ones | ES6 spec, but poor browser support; prevents stack overflow |
| Pattern | Stack Usage | TCO Eligible | When to Use |
|---|---|---|---|
| Standard Recursion | O(n) stack frames | ✗ No (pending operations after call) | Simple problems, small input size |
| Tail Recursion | O(1) with TCO, O(n) without | ✓ Yes (return is direct recursive call) | Large inputs if TCO available |
| Accumulator Pattern | O(1) with TCO | ✓ Yes (pass result through parameters) | Convert standard recursion to tail recursion |
| Iteration (Loop) | O(1) stack | N/A (no recursion) | Production code, guaranteed stack safety |
Example: Basic recursion patterns
// Standard recursion - NOT tail recursive
function factorial(n) {
// Base case
if (n <= 1) return 1;
// Recursive case - operation AFTER recursive call
return n * factorial(n - 1); // ✗ Not tail call (multiplication pending)
}
console.log(factorial(5)); // 120
// Stack: factorial(5) waits for factorial(4)
// factorial(4) waits for factorial(3), etc.
// Tail recursive version - uses accumulator
function factorialTail(n, acc = 1) {
// Base case
if (n <= 1) return acc;
// Recursive case - nothing after recursive call
return factorialTail(n - 1, n * acc); // ✓ Tail call (direct return)
}
console.log(factorialTail(5)); // 120
// With TCO: reuses same stack frame
// Fibonacci - standard recursion (exponential time!)
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // Two recursive calls
}
// Very slow for large n: fib(40) takes seconds
// Fibonacci - tail recursive with two accumulators
function fibTail(n, a = 0, b = 1) {
if (n === 0) return a;
return fibTail(n - 1, b, a + b); // Tail call
}
console.log(fibTail(40)); // Fast even for large n
// Array sum - standard recursion
function sumArray(arr) {
if (arr.length === 0) return 0;
return arr[0] + sumArray(arr.slice(1)); // ✗ Not tail call
}
// Array sum - tail recursive
function sumArrayTail(arr, acc = 0) {
if (arr.length === 0) return acc;
return sumArrayTail(arr.slice(1), acc + arr[0]); // ✓ Tail call
}
Example: Converting to tail recursion
// Problem: countdown - NOT tail recursive
function countdown(n) {
if (n < 0) return;
console.log(n);
countdown(n - 1);
console.log('Done', n); // ✗ Operation after recursive call
}
// Solution: tail recursive countdown
function countdownTail(n) {
if (n < 0) return;
console.log(n);
return countdownTail(n - 1); // ✓ Tail call
}
// Problem: power - NOT tail recursive
function power(base, exp) {
if (exp === 0) return 1;
return base * power(base, exp - 1); // ✗ Multiplication pending
}
// Solution: tail recursive with accumulator
function powerTail(base, exp, acc = 1) {
if (exp === 0) return acc;
return powerTail(base, exp - 1, acc * base); // ✓ Tail call
}
// Tree traversal - naturally recursive
function sumTree(node) {
if (!node) return 0;
return node.value
+ sumTree(node.left)
+ sumTree(node.right);
// Not tail recursive but appropriate for trees
}
// Tail recursive tree traversal (using continuation)
function sumTreeTail(node, cont = x => x) {
if (!node) return cont(0);
return sumTreeTail(node.left, leftSum =>
sumTreeTail(node.right, rightSum =>
cont(node.value + leftSum + rightSum)
)
);
}
Important Limitations:
- TCO Support: Proper tail call optimization is in ES6 spec but only Safari implements it. Chrome/Firefox/Edge don't support TCO.
- Stack Overflow Risk: Without TCO, even tail-recursive functions will overflow stack with large inputs (typically 10,000-15,000 calls).
- Production Code: For critical code, use iteration (loops) instead of recursion to guarantee stack safety.
- Trampoline: Workaround pattern to simulate TCO, but adds complexity.
Example: When to use recursion vs iteration
// ✓ Good use of recursion - tree/graph structures
function findNode(tree, value) {
if (!tree) return null;
if (tree.value === value) return tree;
return findNode(tree.left, value) || findNode(tree.right, value);
}
// ✓ Good use of recursion - divide and conquer
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
if (left > right) return -1;
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
return binarySearch(arr, target, mid + 1, right);
}
// ✗ Bad use of recursion - simple iteration better
function sumToN(n) {
if (n === 0) return 0;
return n + sumToN(n - 1); // Just use n * (n + 1) / 2
}
// ✓ Better: iterative solution
function sumToNIterative(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
// Or: return n * (n + 1) / 2;
}
// Trampoline pattern (TCO workaround)
function trampoline(fn) {
while (typeof fn === 'function') {
fn = fn(); // Keep calling until not a function
}
return fn;
}
function factorialTrampoline(n, acc = 1) {
if (n <= 1) return acc;
return () => factorialTrampoline(n - 1, n * acc); // Return thunk
}
console.log(trampoline(factorialTrampoline(100000))); // No stack overflow
Section 6 Summary
- Function types: Declarations (hoisted), expressions (not hoisted), arrows (lexical this), generators (yield), async (await)
- Arrow functions: Concise syntax, lexical
this, noarguments/new/prototype, ideal for callbacks - Parameters: Default values, rest (
...args), destructuring (array/object), combine for flexible APIs - Higher-order functions: Take/return functions; enables map/filter/reduce, composition, abstraction
- Closures: Functions retaining outer scope access; enables private state, factories, memoization
- IIFE: Immediately-invoked functions; creates private scope, module pattern (pre-ES6 modules)
- call/apply/bind: Control
thiscontext; call/apply execute now, bind returns new function - Recursion: Self-calling functions; elegant for trees/graphs; tail recursion + TCO prevents stack overflow (limited support)
7. Objects and Property Management
7.1 Object Literal Syntax and Computed Properties
| Feature | Syntax | ES Version | Description |
|---|---|---|---|
| Basic Object Literal | { key: value } |
ES3 | Traditional property definition with static keys |
| Property Shorthand ES6 | { name } |
ES6 | Equivalent to { name: name } when variable name matches key |
| Method Shorthand ES6 | { method() {} } |
ES6 | Concise method definition, equivalent to { method: function() {} } |
| Computed Property ES6 | { [expression]: value } |
ES6 | Property name computed at runtime from expression |
| Computed Method ES6 | { [expr]() {} } |
ES6 | Method name computed dynamically |
| Spread Properties ES2018 | { ...obj } |
ES2018 | Copy enumerable properties from source object |
| Getters/Setters | { get prop() {}, set prop(v) {} } |
ES5 | Define accessor properties with custom logic |
Example: Modern object literal syntax
// ES6+ object literal features
const name = 'Alice';
const age = 30;
const propKey = 'dynamicKey';
const user = {
// Property shorthand (ES6)
name, // Same as name: name
age, // Same as age: age
// Method shorthand (ES6)
greet() {
return `Hello, I'm ${this.name}`;
},
// Computed property name (ES6)
[propKey]: 'dynamic value',
[`computed_${name}`]: true,
// Computed method name (ES6)
[`get${name}Info`]() {
return `${this.name} is ${this.age}`;
},
// Traditional syntax still works
email: 'alice@example.com',
// Arrow function as property (not a method)
arrowFunc: () => 'arrow',
// Getter and setter
get fullInfo() {
return `${this.name}, ${this.age} years old`;
},
set fullInfo(info) {
[this.name, this.age] = info.split(', ');
}
};
console.log(user.name); // 'Alice'
console.log(user.greet()); // 'Hello, I'm Alice'
console.log(user.dynamicKey); // 'dynamic value'
console.log(user.computed_Alice); // true
console.log(user.getAliceInfo()); // 'Alice is 30'
console.log(user.fullInfo); // 'Alice, 30 years old'
Example: Computed properties in practice
// Dynamic property names
const fields = ['id', 'name', 'email'];
const values = [1, 'Bob', 'bob@example.com'];
// Build object with computed properties
const user = fields.reduce((obj, field, index) => {
obj[field] = values[index];
return obj;
}, {});
// { id: 1, name: 'Bob', email: 'bob@example.com' }
// Using computed properties in literal
const status = 'active';
const config = {
[`is${status.charAt(0).toUpperCase() + status.slice(1)}`]: true
};
// { isActive: true }
// Symbol as computed property
const SECRET = Symbol('secret');
const obj = {
public: 'visible',
[SECRET]: 'hidden' // Property name is a symbol
};
console.log(obj.public); // 'visible'
console.log(obj[SECRET]); // 'hidden'
console.log(Object.keys(obj)); // ['public'] - symbols not enumerable
// Spread properties (ES2018)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'fr', fontSize: 14 }
// userPrefs.lang overwrites defaults.lang
7.2 Property Descriptors and Object.defineProperty
| Descriptor Attribute | Type | Default (defineProperty) | Default (Literal) | Description |
|---|---|---|---|---|
| value | any | undefined |
property value | The property's value (data descriptor) |
| writable | boolean | false |
true |
Can property value be changed? |
| enumerable | boolean | false |
true |
Shows in for...in, Object.keys()? |
| configurable | boolean | false |
true |
Can descriptor be changed? Can property be deleted? |
| get | function | undefined |
N/A | Getter function (accessor descriptor) |
| set | function | undefined |
N/A | Setter function (accessor descriptor) |
| Method | Syntax | Description |
|---|---|---|
| Object.defineProperty() | Object.defineProperty(obj, prop, descriptor) |
Define or modify single property with full control over attributes |
| Object.defineProperties() | Object.defineProperties(obj, descriptors) |
Define or modify multiple properties at once |
| Object.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor(obj, prop) |
Get descriptor object for single property |
| Object.getOwnPropertyDescriptors() ES2017 | Object.getOwnPropertyDescriptors(obj) |
Get descriptors for all own properties |
Example: Property descriptors in action
const obj = {};
// Define property with custom descriptor
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false, // Cannot change value
enumerable: true, // Shows in for...in, Object.keys()
configurable: false // Cannot delete or reconfigure
});
obj.name = 'Bob'; // Fails silently (strict mode: TypeError)
console.log(obj.name); // 'Alice' (unchanged)
delete obj.name; // Fails silently (configurable: false)
console.log(obj.name); // 'Alice' (still exists)
// Check property descriptor
const desc = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(desc);
// { value: 'Alice', writable: false, enumerable: true, configurable: false }
// Compare with literal property (all attributes true)
const obj2 = { name: 'Charlie' };
const desc2 = Object.getOwnPropertyDescriptor(obj2, 'name');
console.log(desc2);
// { value: 'Charlie', writable: true, enumerable: true, configurable: true }
Example: Accessor properties with get/set
const person = {
firstName: 'John',
lastName: 'Doe'
};
// Define computed property with getter/setter
Object.defineProperty(person, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true
});
console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
// Define multiple properties at once
const account = {};
Object.defineProperties(account, {
_balance: {
value: 0,
writable: true,
enumerable: false // Private-like (convention)
},
balance: {
get() { return this._balance; },
set(val) {
if (val < 0) throw new Error('Balance cannot be negative');
this._balance = val;
},
enumerable: true
},
currency: {
value: 'USD',
writable: false,
enumerable: true
}
});
console.log(account.balance); // 0
account.balance = 100;
console.log(account.balance); // 100
// account.balance = -50; // Error: Balance cannot be negative
Example: Non-enumerable and non-configurable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false // Won't show in iteration
});
console.log(obj.hidden); // 'secret' (accessible directly)
console.log(Object.keys(obj)); // ['a', 'b'] (hidden not listed)
for (let key in obj) {
console.log(key); // Only logs 'a' and 'b'
}
// Non-configurable property
Object.defineProperty(obj, 'permanent', {
value: 'cannot change',
configurable: false
});
// Cannot reconfigure
try {
Object.defineProperty(obj, 'permanent', {
value: 'new value' // TypeError: cannot redefine
});
} catch (e) {
console.log('Cannot reconfigure');
}
// Cannot delete
delete obj.permanent; // Fails silently
console.log(obj.permanent); // Still exists
// Get all descriptors (ES2017)
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(allDescriptors.hidden);
// { value: 'secret', writable: false, enumerable: false, configurable: false }
Important: Data descriptors (value/writable) and accessor descriptors (get/set) are mutually
exclusive. A property can have
value OR get/set, not both. Non-configurable properties
cannot be deleted or have their descriptor changed (except writable can go from true to false).
7.3 Object Methods (Object.keys, Object.values, Object.entries)
| Method | Returns | ES Version | Description |
|---|---|---|---|
| Object.keys(obj) | Array of strings | ES5 | Own enumerable property names (keys) |
| Object.values(obj) ES2017 | Array of values | ES2017 | Own enumerable property values |
| Object.entries(obj) ES2017 | Array of [key, value] pairs | ES2017 | Own enumerable properties as key-value pairs |
| Object.fromEntries(entries) ES2019 | Object | ES2019 | Create object from array of [key, value] pairs (inverse of entries) |
| Object.getOwnPropertyNames(obj) | Array of strings | ES5 | All own property names (including non-enumerable) |
| Object.getOwnPropertySymbols(obj) ES6 | Array of symbols | ES6 | All own symbol properties |
| Object.hasOwn(obj, prop) ES2022 | boolean | ES2022 | Safe alternative to hasOwnProperty (recommended) |
| obj.hasOwnProperty(prop) | boolean | ES3 | Check if property is own (not inherited); use Object.hasOwn instead |
| Comparison | for...in | Object.keys() | Object.getOwnPropertyNames() |
|---|---|---|---|
| Own Properties | ✓ Yes | ✓ Yes | ✓ Yes |
| Inherited Properties | ✓ Yes (from prototype chain) | ✗ No | ✗ No |
| Non-enumerable | ✗ No | ✗ No | ✓ Yes |
| Symbol Properties | ✗ No | ✗ No | ✗ No (use getOwnPropertySymbols) |
Example: Object.keys, values, entries
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true
};
// Get all keys
const keys = Object.keys(user);
console.log(keys); // ['id', 'name', 'email', 'active']
// Get all values (ES2017)
const values = Object.values(user);
console.log(values); // [1, 'Alice', 'alice@example.com', true]
// Get key-value pairs (ES2017)
const entries = Object.entries(user);
console.log(entries);
// [['id', 1], ['name', 'Alice'], ['email', 'alice@example.com'], ['active', true]]
// Iterate over entries
Object.entries(user).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// Transform object using entries
const doubled = Object.fromEntries(
Object.entries({ a: 1, b: 2, c: 3 })
.map(([key, value]) => [key, value * 2])
);
console.log(doubled); // { a: 2, b: 4, c: 6 }
// Filter object properties
const activeUsers = Object.fromEntries(
Object.entries({ u1: { active: true }, u2: { active: false } })
.filter(([key, val]) => val.active)
);
// { u1: { active: true } }
Example: Enumerable vs non-enumerable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false
});
// Add symbol property
const sym = Symbol('symbol');
obj[sym] = 'symbol value';
console.log(Object.keys(obj)); // ['a', 'b'] (only enumerable string keys)
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'hidden'] (includes non-enumerable)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(symbol)]
// for...in includes inherited enumerable properties
function Parent() {}
Parent.prototype.inherited = 'from parent';
const child = Object.create(new Parent());
child.own = 'own property';
for (let key in child) {
console.log(key); // Logs: 'own', 'inherited'
}
// Filter to own properties only
for (let key in child) {
if (Object.hasOwn(child, key)) { // ES2022
console.log(key); // Logs: 'own' only
}
}
// Alternative: hasOwnProperty (older)
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // Logs: 'own' only
}
}
Example: Practical use cases
// Count object properties
const obj = { a: 1, b: 2, c: 3 };
const count = Object.keys(obj).length; // 3
// Check if object is empty
const isEmpty = Object.keys(obj).length === 0;
// Merge objects by converting to entries
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.fromEntries([
...Object.entries(obj1),
...Object.entries(obj2)
]);
// { a: 1, b: 3, c: 4 } (obj2 overwrites obj1)
// Convert object to Map
const map = new Map(Object.entries(obj));
// Convert Map to object
const objFromMap = Object.fromEntries(map);
// Swap keys and values
const original = { a: 1, b: 2, c: 3 };
const swapped = Object.fromEntries(
Object.entries(original).map(([k, v]) => [v, k])
);
// { 1: 'a', 2: 'b', 3: 'c' }
// Pick specific properties
function pick(obj, keys) {
return Object.fromEntries(
keys.filter(key => key in obj)
.map(key => [key, obj[key]])
);
}
const user = { id: 1, name: 'Alice', email: 'a@example.com', age: 30 };
const userPreview = pick(user, ['id', 'name']);
// { id: 1, name: 'Alice' }
7.4 Object Creation Patterns (Object.create, constructors)
| Pattern | Syntax | Prototype | Use Case |
|---|---|---|---|
| Object Literal | const obj = { } |
Object.prototype | Simple data objects, quick creation |
| Object() Constructor | const obj = new Object() |
Object.prototype | Rarely used (literal preferred) |
| Object.create() ES5 | Object.create(proto, descriptors?) |
Specified proto | Prototypal inheritance, null prototype objects |
| Constructor Function | function Ctor() { } + new Ctor() |
Ctor.prototype | Pre-ES6 classes, instance creation with shared methods |
| Factory Function | function create() { return { }; } |
Object.prototype (or custom) | Object creation without 'new', private state |
| Class Syntax ES6 | class C { } + new C() |
C.prototype | Modern OOP, cleaner syntax over constructors |
| Method | Description | Returns |
|---|---|---|
| Object.create(proto) | Create new object with specified prototype | New object inheriting from proto |
| Object.create(proto, descriptors) | Create object with prototype and property descriptors | New object with specified properties |
| Object.create(null) | Create object with no prototype (truly empty) | Object without any inherited properties |
| Object.setPrototypeOf(obj, proto) | Change prototype of existing object (slow, avoid) | The modified object |
| Object.getPrototypeOf(obj) | Get prototype of object | Prototype object or null |
Example: Object.create() patterns
// Create object with specific prototype
const personProto = {
greet() {
return `Hello, I'm ${this.name}`;
}
};
const person = Object.create(personProto);
person.name = 'Alice';
console.log(person.greet()); // "Hello, I'm Alice"
console.log(Object.getPrototypeOf(person) === personProto); // true
// Create with property descriptors
const user = Object.create(personProto, {
name: {
value: 'Bob',
writable: true,
enumerable: true
},
age: {
value: 30,
writable: true,
enumerable: true
}
});
console.log(user.name); // 'Bob'
console.log(user.greet()); // "Hello, I'm Bob"
// Create object with null prototype (no inherited properties)
const dict = Object.create(null);
dict.key = 'value';
console.log(dict.toString); // undefined (no inherited methods)
console.log(dict.hasOwnProperty); // undefined
// Useful for pure data storage, no prototype pollution
// Compare with literal
const literal = {};
console.log(literal.toString); // [Function: toString] (inherited)
console.log(Object.getPrototypeOf(literal) === Object.prototype); // true
Example: Constructor functions
// Constructor function (pre-ES6 classes)
function Person(name, age) {
// Instance properties (unique per instance)
this.name = name;
this.age = age;
}
// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
return this.age;
};
// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.greet()); // "Hello, I'm Bob"
console.log(alice.greet === bob.greet); // true (shared method)
// Verify prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice instanceof Person); // true
// Forgetting 'new' - common mistake
function SafeConstructor(name) {
// Guard against missing 'new'
if (!(this instanceof SafeConstructor)) {
return new SafeConstructor(name);
}
this.name = name;
}
const instance1 = new SafeConstructor('Alice');
const instance2 = SafeConstructor('Bob'); // Works without 'new'
console.log(instance2 instanceof SafeConstructor); // true
Example: Factory functions
// Factory function - no 'new' needed
function createPerson(name, age) {
// Private variables (closure)
let privateData = 'secret';
// Return object with public interface
return {
name,
age,
greet() {
return `Hello, I'm ${this.name}`;
},
getPrivate() {
return privateData; // Access private via closure
}
};
}
const person1 = createPerson('Alice', 30); // No 'new'
const person2 = createPerson('Bob', 25);
console.log(person1.greet()); // "Hello, I'm Alice"
console.log(person1.getPrivate()); // 'secret'
console.log(person1.privateData); // undefined (truly private)
// Factory with prototypal inheritance
function createUser(name) {
const proto = {
greet() { return `Hello ${this.name}`; }
};
const user = Object.create(proto);
user.name = name;
return user;
}
const user = createUser('Charlie');
console.log(user.greet()); // "Hello Charlie"
// Modern: ES6 class (preferred over constructors)
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
const personInstance = new PersonClass('Dave', 35);
console.log(personInstance.greet()); // "Hello, I'm Dave"
Best Practice: Modern JavaScript prefers ES6 classes over constructor functions for clarity.
Use factory functions when you need private state via closures or want to avoid
new. Use
Object.create(null) for dictionaries/maps to avoid prototype pollution. Avoid
Object.setPrototypeOf() as it's very slow.
7.5 Object Cloning and Merging (Object.assign, spread)
| Method | Syntax | Copy Type | Nested Objects |
|---|---|---|---|
| Object.assign() ES6 | Object.assign(target, ...sources) |
Shallow copy | Copies references (not deep) |
| Spread Operator ES2018 | { ...obj } |
Shallow copy | Copies references (not deep) |
| JSON.parse/stringify | JSON.parse(JSON.stringify(obj)) |
Deep copy | ✓ Clones nested, but loses functions/symbols/dates |
| structuredClone() NEW | structuredClone(obj) |
Deep copy | ✓ Clones nested, preserves types (not functions) |
| Manual Deep Clone | Recursive function | Deep copy | ✓ Full control, handle all types |
| Feature | Object.assign() | Spread {...} | Differences |
|---|---|---|---|
| Copy enumerable props | ✓ Yes | ✓ Yes | Both copy own enumerable properties |
| Trigger setters | ✓ Yes | ✗ No | assign() invokes setters, spread defines new props |
| Copy getters/setters | ✗ No (executes getter) | ✗ No (executes getter) | Both execute getters, copy resulting values |
| Mutate target | ✓ Yes (modifies first arg) | ✗ No (creates new object) | assign() mutates, spread creates new |
| Syntax | Function call | Operator (concise) | Spread is more concise and readable |
Example: Shallow copying with Object.assign and spread
const original = {
name: 'Alice',
age: 30,
address: { city: 'NYC', zip: '10001' }
};
// Object.assign - shallow copy
const copy1 = Object.assign({}, original);
copy1.name = 'Bob';
copy1.address.city = 'LA'; // Modifies nested object
console.log(original.name); // 'Alice' (primitive copied)
console.log(original.address.city); // 'LA' (nested reference shared!)
// Spread operator - shallow copy (ES2018)
const copy2 = { ...original };
copy2.age = 25;
copy2.address.zip = '90001'; // Also modifies original!
console.log(original.age); // 30 (primitive copied)
console.log(original.address.zip); // '90001' (nested reference shared!)
// Merge multiple objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { c: 5, d: 6 };
// Using Object.assign
const merged1 = Object.assign({}, obj1, obj2, obj3);
// { a: 1, b: 3, c: 5, d: 6 } (later values overwrite)
// Using spread (more common)
const merged2 = { ...obj1, ...obj2, ...obj3 };
// { a: 1, b: 3, c: 5, d: 6 } (same result)
// Add/override properties while spreading
const enhanced = {
...original,
age: 35, // Override
email: 'alice@example.com' // Add new
};
// { name: 'Alice', age: 35, address: {...}, email: '...' }
Example: Deep cloning techniques
const original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding'],
address: { city: 'NYC', coords: { lat: 40, lng: -74 } },
createdAt: new Date('2024-01-01')
};
// Method 1: JSON (quick but lossy)
const jsonClone = JSON.parse(JSON.stringify(original));
jsonClone.hobbies.push('gaming');
jsonClone.address.city = 'LA';
console.log(original.hobbies); // ['reading', 'coding'] (not affected)
console.log(original.address.city); // 'NYC' (not affected)
console.log(jsonClone.createdAt); // String, not Date! (limitation)
// Method 2: structuredClone() (modern, recommended)
const structClone = structuredClone(original);
structClone.address.coords.lat = 35;
console.log(original.address.coords.lat); // 40 (not affected)
console.log(structClone.createdAt instanceof Date); // true (preserved!)
// Limitations: cannot clone functions
const withFunction = {
data: 'value',
method() { return this.data; }
};
// structuredClone(withFunction); // Error: functions not cloneable
// Method 3: Manual deep clone (full control)
function deepClone(obj, seen = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') return obj;
// Handle circular references
if (seen.has(obj)) return seen.get(obj);
// Handle Date
if (obj instanceof Date) return new Date(obj);
// Handle Array
if (Array.isArray(obj)) {
const arrCopy = [];
seen.set(obj, arrCopy);
obj.forEach((item, i) => {
arrCopy[i] = deepClone(item, seen);
});
return arrCopy;
}
// Handle Object
const objCopy = {};
seen.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], seen);
});
return objCopy;
}
const manualClone = deepClone(original);
manualClone.address.coords.lng = -118;
console.log(original.address.coords.lng); // -74 (not affected)
Example: Object.assign vs spread differences
// Difference 1: Mutating vs creating
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source); // Mutates target
console.log(target); // { a: 1, b: 2 } (modified)
const newObj = { ...target, ...source }; // Creates new object
// target unchanged
// Difference 2: Setters
const obj = {
_value: 0,
set value(v) {
console.log('Setter called:', v);
this._value = v;
}
};
// Object.assign triggers setter
Object.assign(obj, { value: 10 }); // Logs: "Setter called: 10"
// Spread defines new property (doesn't trigger setter)
const copy = { ...obj, value: 20 }; // No setter call
console.log(copy.value); // 20 (simple property, not setter)
// Use case: Default options
function configure(options = {}) {
const defaults = {
timeout: 5000,
retries: 3,
cache: true
};
// Merge with user options (spread preferred)
return { ...defaults, ...options };
}
const config1 = configure({ timeout: 10000 });
// { timeout: 10000, retries: 3, cache: true }
const config2 = configure({ retries: 5, debug: true });
// { timeout: 5000, retries: 5, cache: true, debug: true }
Warning: Both
Object.assign() and spread create shallow
copies. Nested objects are copied by reference, not value. Modifying nested properties affects the
original. For true deep copies, use structuredClone() (modern) or implement custom deep clone
logic.
7.6 Property Getters and Setters
| Syntax Form | Example | When to Use |
|---|---|---|
| Object Literal Getter | { get prop() { return value; } } |
Computed properties, validation on read |
| Object Literal Setter | { set prop(val) { this._prop = val; } } |
Validation/transformation on write |
| defineProperty Getter | Object.defineProperty(obj, 'prop', { get() {} }) |
Add getter to existing object |
| defineProperty Setter | Object.defineProperty(obj, 'prop', { set(v) {} }) |
Add setter to existing object |
| Class Getter | class C { get prop() { } } |
ES6 class computed properties |
| Class Setter | class C { set prop(v) { } } |
ES6 class property validation |
| Use Case | Pattern | Example |
|---|---|---|
| Computed Property | Getter derives value from other properties | get fullName() { return this.first + ' ' + this.last; } |
| Validation | Setter validates before assignment | set age(v) { if (v < 0) throw Error; this._age = v; } |
| Lazy Initialization | Getter computes value on first access | get data() { return this._data || (this._data = load()); } |
| Side Effects | Setter triggers additional actions | set value(v) { this._value = v; this.notify(); } |
| Read-only Property | Getter without setter | get id() { return this._id; } (no setter) |
| Private Backing Field | Store value in _property, expose via getter/setter | get name() { return this._name; } |
Example: Getters and setters in object literals
const person = {
firstName: 'John',
lastName: 'Doe',
_age: 30, // Backing field (convention: underscore = private-like)
// Getter: computed property
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter: parse and set multiple properties
set fullName(name) {
const parts = name.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
},
// Getter with validation/transformation
get age() {
return this._age;
},
// Setter with validation
set age(value) {
if (typeof value !== 'number' || value < 0 || value > 150) {
throw new Error('Invalid age');
}
this._age = value;
},
// Read-only property (getter only)
get birthYear() {
return new Date().getFullYear() - this._age;
}
};
// Using getters and setters
console.log(person.fullName); // "John Doe" (getter called)
person.fullName = 'Jane Smith'; // Setter called
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
console.log(person.age); // 30 (getter)
person.age = 25; // Setter with validation
// person.age = -5; // Error: Invalid age
console.log(person.birthYear); // Computed from age
// person.birthYear = 1990; // TypeError: no setter (read-only)
Example: Getters/setters in classes
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
// Getter for computed property
get area() {
return this._width * this._height;
}
get perimeter() {
return 2 * (this._width + this._height);
}
// Getter/setter with validation
get width() {
return this._width;
}
set width(value) {
if (value <= 0) {
throw new Error('Width must be positive');
}
this._width = value;
}
get height() {
return this._height;
}
set height(value) {
if (value <= 0) {
throw new Error('Height must be positive');
}
this._height = value;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50 (computed)
console.log(rect.perimeter); // 30 (computed)
rect.width = 20; // Setter with validation
console.log(rect.area); // 100 (auto-updated)
// rect.width = -5; // Error: Width must be positive
// rect.area = 200; // TypeError: no setter (read-only)
Example: Advanced getter/setter patterns
// Lazy initialization with getter
class DataLoader {
constructor(url) {
this.url = url;
this._data = null;
}
get data() {
// Load data on first access, cache for subsequent access
if (this._data === null) {
console.log('Loading data...');
this._data = this.loadData(); // Expensive operation
}
return this._data;
}
loadData() {
// Simulate data loading
return { loaded: true, url: this.url };
}
}
const loader = new DataLoader('/api/data');
console.log(loader.data); // Logs: "Loading data...", returns data
console.log(loader.data); // No log, returns cached data
// Observable pattern with setters
class Observable {
constructor() {
this._observers = [];
this._value = null;
}
get value() {
return this._value;
}
set value(newValue) {
const oldValue = this._value;
this._value = newValue;
this.notify(oldValue, newValue);
}
subscribe(observer) {
this._observers.push(observer);
}
notify(oldValue, newValue) {
this._observers.forEach(fn => fn(newValue, oldValue));
}
}
const obs = new Observable();
obs.subscribe((newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`);
});
obs.value = 10; // Logs: "Changed from null to 10"
obs.value = 20; // Logs: "Changed from 10 to 20"
// Type conversion in setter
const config = {
_port: 8080,
get port() {
return this._port;
},
set port(value) {
// Always store as number
this._port = Number(value);
}
};
config.port = '3000'; // String input
console.log(config.port); // 3000 (number)
console.log(typeof config.port); // 'number'
Best Practice: Use getters for computed properties and derived state. Use setters for
validation, type conversion, and triggering side effects. Follow naming convention: use underscore prefix for
backing fields (e.g.,
_value). For true privacy in modern JS, use private fields
(#field) instead of convention.
7.7 Object Freezing and Sealing (freeze, seal, preventExtensions)
| Method | Add Props | Delete Props | Modify Values | Change Config |
|---|---|---|---|---|
| Normal Object | ✓ Yes | ✓ Yes | ✓ Yes | ✓ Yes |
| Object.preventExtensions() | ✗ No | ✓ Yes | ✓ Yes | ✓ Yes |
| Object.seal() | ✗ No | ✗ No | ✓ Yes | ✗ No |
| Object.freeze() | ✗ No | ✗ No | ✗ No | ✗ No |
| Method | Effect | Check Method | Use Case |
|---|---|---|---|
| Object.preventExtensions(obj) | Cannot add new properties | Object.isExtensible(obj) |
Prevent accidental property additions |
| Object.seal(obj) | Cannot add/delete, can modify existing | Object.isSealed(obj) |
Fixed property set, mutable values |
| Object.freeze(obj) | Completely immutable (shallow) | Object.isFrozen(obj) |
Constants, immutable config |
Example: Object.preventExtensions()
const obj = { a: 1, b: 2 };
// Prevent adding new properties
Object.preventExtensions(obj);
// Existing properties work normally
obj.a = 10; // ✓ Works (can modify)
delete obj.b; // ✓ Works (can delete)
console.log(obj); // { a: 10 }
// Cannot add new properties
obj.c = 3; // ✗ Fails silently (strict mode: TypeError)
console.log(obj.c); // undefined
// Check if extensible
console.log(Object.isExtensible(obj)); // false
// Cannot make extensible again (one-way operation)
// No Object.makeExtensible() method
Example: Object.seal()
const user = { name: 'Alice', age: 30 };
// Seal object - fixed property set
Object.seal(user);
// Can modify existing properties
user.name = 'Bob'; // ✓ Works
user.age = 35; // ✓ Works
console.log(user); // { name: 'Bob', age: 35 }
// Cannot add new properties
user.email = 'test@example.com'; // ✗ Fails silently
console.log(user.email); // undefined
// Cannot delete properties
delete user.age; // ✗ Fails silently
console.log(user.age); // 35 (still exists)
// Cannot reconfigure property descriptors
// Object.defineProperty(user, 'name', { enumerable: false }); // ✗ TypeError
// Check if sealed
console.log(Object.isSealed(user)); // true
// Sealed implies non-extensible
console.log(Object.isExtensible(user)); // false
Example: Object.freeze()
const config = {
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
MAX_RETRIES: 3
};
// Freeze object - completely immutable
Object.freeze(config);
// Cannot modify properties
config.TIMEOUT = 10000; // ✗ Fails silently (strict mode: TypeError)
console.log(config.TIMEOUT); // 5000 (unchanged)
// Cannot add properties
config.DEBUG = true; // ✗ Fails silently
console.log(config.DEBUG); // undefined
// Cannot delete properties
delete config.MAX_RETRIES; // ✗ Fails silently
console.log(config.MAX_RETRIES); // 3 (still exists)
// Check if frozen
console.log(Object.isFrozen(config)); // true
// Frozen implies sealed and non-extensible
console.log(Object.isSealed(config)); // true
console.log(Object.isExtensible(config)); // false
// Common use: constants
const CONSTANTS = Object.freeze({
PI: 3.14159,
E: 2.71828,
MAX_INT: 2147483647
});
// CONSTANTS.PI = 3.14; // ✗ Cannot modify
Example: Shallow vs deep freeze
// Object.freeze is SHALLOW - nested objects not frozen
const user = {
name: 'Alice',
settings: {
theme: 'dark',
notifications: true
}
};
Object.freeze(user);
// Top-level frozen
user.name = 'Bob'; // ✗ Fails
console.log(user.name); // 'Alice' (unchanged)
// Nested object NOT frozen
user.settings.theme = 'light'; // ✓ Works!
user.settings.notifications = false; // ✓ Works!
console.log(user.settings.theme); // 'light' (modified)
// Deep freeze implementation
function deepFreeze(obj) {
// Freeze object itself
Object.freeze(obj);
// Recursively freeze all object properties
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = obj[prop];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return obj;
}
const deepUser = {
name: 'Charlie',
settings: { theme: 'dark', notifications: true }
};
deepFreeze(deepUser);
// Now nested is also frozen
deepUser.settings.theme = 'light'; // ✗ Fails
console.log(deepUser.settings.theme); // 'dark' (unchanged)
Example: Practical use cases
// Use case 1: Enum-like constants
const Status = Object.freeze({
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected'
});
// Status.PENDING = 'other'; // ✗ Cannot modify
// Use case 2: Configuration objects
const appConfig = Object.freeze({
version: '1.0.0',
apiEndpoint: '/api/v1',
features: {
auth: true,
analytics: true
}
});
// Use case 3: Prevent accidental mutations in function
function processUser(user) {
// Prevent function from modifying passed object
const immutableUser = Object.freeze({ ...user });
// immutableUser.name = 'Modified'; // ✗ Fails
// Return new object instead
return { ...immutableUser, processed: true };
}
// Use case 4: Sealed object with mutable values
const counters = Object.seal({
clicks: 0,
views: 0,
purchases: 0
});
// Can update counters
counters.clicks++;
counters.views += 10;
// But cannot add new counters
// counters.downloads = 0; // ✗ Fails
// Comparison matrix
const obj1 = { a: 1 };
Object.preventExtensions(obj1);
console.log({
canAdd: !Object.isExtensible(obj1), // true (cannot add)
canDelete: !Object.isSealed(obj1), // true (can delete)
canModify: !Object.isFrozen(obj1) // true (can modify)
});
const obj2 = { a: 1 };
Object.seal(obj2);
console.log({
canAdd: !Object.isExtensible(obj2), // true (cannot add)
canDelete: !Object.isSealed(obj2), // false (cannot delete)
canModify: !Object.isFrozen(obj2) // true (can modify)
});
const obj3 = { a: 1 };
Object.freeze(obj3);
console.log({
canAdd: !Object.isExtensible(obj3), // true (cannot add)
canDelete: !Object.isSealed(obj3), // false (cannot delete)
canModify: !Object.isFrozen(obj3) // false (cannot modify)
});
Important Limitations:
- Shallow only: All three methods (preventExtensions, seal, freeze) only affect the immediate object, not nested objects
- Irreversible: Cannot undo these operations - once frozen/sealed, always frozen/sealed
- Silent failures: In non-strict mode, violations fail silently (no error thrown)
- Performance: Frozen objects may have optimization benefits but are rarely the bottleneck
Section 7 Summary
- Modern literals: Property/method shorthand, computed properties, spread syntax, getters/setters for clean object creation
- Property descriptors: Control writable/enumerable/configurable attributes; defineProperty for fine-grained control
- Object methods: keys/values/entries for iteration; fromEntries for transformation; hasOwn for safe property checks
- Creation patterns: Object.create for prototypal inheritance; constructors (legacy) vs classes (modern); factory functions for flexibility
- Cloning: Spread/assign for shallow copy; structuredClone for deep copy; JSON for simple deep copy (lossy)
- Getters/setters: Computed properties, validation, lazy initialization; use _prefix convention for backing fields
- Immutability: preventExtensions (no add), seal (no add/delete), freeze (no changes); all shallow, implement deepFreeze for nested
8. Prototypes and Inheritance System
8.1 Prototype Chain and Property Lookup
| Concept | Description | Key Points |
|---|---|---|
| Prototype | Internal reference ([[Prototype]]) that links object to another object | Forms inheritance chain, enables property sharing |
| Prototype Chain | Series of prototype links from object to Object.prototype to null | Property lookup traverses chain until found or reaches null |
| __proto__ | Accessor property to get/set [[Prototype]] (legacy, avoid) | Non-standard but widely supported; use Object.getPrototypeOf() instead |
| Object.prototype | Top of prototype chain for normal objects; contains toString, hasOwnProperty, etc. | Inherits from null; all objects inherit from it unless explicitly set |
| Function.prototype | Prototype for all functions; contains call, apply, bind, etc. | Functions inherit from Function.prototype which inherits from Object.prototype |
| .prototype Property | Property on constructor functions; used as prototype for instances created with new | Only constructor functions have .prototype; instances have [[Prototype]] |
| Property Lookup Step | Location | Action |
|---|---|---|
| 1. Own Properties | Object itself | Check if property exists directly on object; return if found |
| 2. Prototype | Object's [[Prototype]] | If not found, check prototype; return if found |
| 3. Prototype Chain | Prototype's prototype, etc. | Continue up chain until property found |
| 4. Object.prototype | Top of chain | Check Object.prototype; return if found |
| 5. null | End of chain | Return undefined if not found anywhere |
Example: Understanding the prototype chain
// Every object has internal [[Prototype]] link
const obj = { a: 1 };
// Access prototype using Object.getPrototypeOf()
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// Object.prototype's prototype is null (end of chain)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// Property lookup demonstration
const animal = {
eats: true,
walk() {
console.log('Animal walks');
}
};
const rabbit = {
jumps: true
};
// Set rabbit's prototype to animal
Object.setPrototypeOf(rabbit, animal);
// rabbit can access both own and inherited properties
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
// Property lookup order
console.log(rabbit.toString); // [Function: toString] (from Object.prototype)
// Chain: rabbit -> animal -> Object.prototype -> null
console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Example: Property shadowing and prototype lookup
const parent = {
name: 'Parent',
greet() {
return `Hello from ${this.name}`;
}
};
const child = Object.create(parent);
child.age = 10;
// Access inherited property
console.log(child.name); // 'Parent' (from prototype)
console.log(child.greet()); // "Hello from Parent"
// Own property shadows prototype property
child.name = 'Child';
console.log(child.name); // 'Child' (own property, shadows prototype)
// Check property ownership
console.log(child.hasOwnProperty('name')); // true (own)
console.log(child.hasOwnProperty('greet')); // false (inherited)
console.log('greet' in child); // true (checks own + prototype)
// Delete own property - prototype property revealed
delete child.name;
console.log(child.name); // 'Parent' (prototype property no longer shadowed)
// Writing to inherited property creates own property
const obj = Object.create({ x: 1 });
console.log(obj.x); // 1 (inherited)
obj.x = 2; // Creates own property (doesn't modify prototype)
console.log(obj.x); // 2 (own property)
console.log(Object.getPrototypeOf(obj).x); // 1 (prototype unchanged)
Example: Working with prototypes
// Get prototype of object
const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true
// Set prototype (avoid in production - slow!)
const animal = { type: 'animal' };
const dog = { breed: 'Labrador' };
Object.setPrototypeOf(dog, animal);
console.log(dog.type); // 'animal' (inherited)
// Create object with specific prototype (preferred)
const cat = Object.create(animal);
cat.breed = 'Persian';
console.log(cat.type); // 'animal' (inherited)
// Check if object is in prototype chain
console.log(animal.isPrototypeOf(dog)); // true
console.log(animal.isPrototypeOf(cat)); // true
console.log(Object.prototype.isPrototypeOf(dog)); // true (all objects)
// Create object with null prototype (no inheritance)
const noProto = Object.create(null);
console.log(noProto.toString); // undefined (no Object.prototype)
console.log(Object.getPrototypeOf(noProto)); // null
// Prototype chain visualization
function showChain(obj, label = 'obj') {
console.log(`${label}:`, obj);
const proto = Object.getPrototypeOf(obj);
if (proto) {
showChain(proto, `${label}.__proto__`);
} else {
console.log('End of chain: null');
}
}
const myObj = { x: 1 };
showChain(myObj);
// myObj: { x: 1 }
// myObj.__proto__: Object.prototype
// End of chain: null
Important: Property lookup is read-only up the chain. Writing to a property always
creates/modifies own property (unless setter exists in prototype). Use
Object.getPrototypeOf() and
Object.create() instead of deprecated __proto__. Avoid
Object.setPrototypeOf() for performance reasons.
8.2 Constructor Functions and new Operator
| Concept | Description | Behavior |
|---|---|---|
| Constructor Function | Regular function used with new to create instances |
Conventionally capitalized; sets up instance properties |
| new Operator | Creates instance, sets prototype, binds this, returns object |
4-step process: create, link, execute, return |
| Constructor.prototype | Object that becomes prototype of instances created with new Constructor() |
Shared methods placed here for memory efficiency |
| constructor Property | Property on prototype pointing back to constructor function | Allows instances to identify their constructor |
| Instance Properties | Properties unique to each instance (set in constructor) | Defined using this.property = value |
| Prototype Methods | Methods shared across all instances (on Constructor.prototype) | Memory efficient - one copy shared by all instances |
| Step | new Operator Process | Effect |
|---|---|---|
| 1. Create | New empty object created | const obj = {} |
| 2. Link Prototype | Object's [[Prototype]] set to Constructor.prototype | Object.setPrototypeOf(obj, Constructor.prototype) |
| 3. Execute Constructor | Constructor called with this bound to new object |
Constructor.call(obj, ...args) |
| 4. Return | Return object (or constructor's return if object) | Returns this unless constructor explicitly returns object |
Example: Constructor functions and instances
// Constructor function (capitalized by convention)
function Person(name, age) {
// Instance properties (unique per instance)
this.name = name;
this.age = age;
// ✗ Avoid methods here (creates new function per instance)
// this.greet = function() { return `Hi, I'm ${this.name}`; };
}
// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
return this.age;
};
// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
// Each instance has own properties
console.log(alice.name); // 'Alice'
console.log(bob.name); // 'Bob'
// Shared methods from prototype
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.greet()); // "Hello, I'm Bob"
// Methods are shared (same function object)
console.log(alice.greet === bob.greet); // true (memory efficient)
// Verify prototype relationship
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice.constructor === Person); // true
console.log(alice instanceof Person); // true
// Prototype chain: alice -> Person.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
Example: Manual simulation of 'new' operator
// Understanding what 'new' does
function myNew(Constructor, ...args) {
// 1. Create new object
const obj = {};
// 2. Link to prototype
Object.setPrototypeOf(obj, Constructor.prototype);
// Or: obj.__proto__ = Constructor.prototype;
// 3. Execute constructor with 'this' bound to new object
const result = Constructor.apply(obj, args);
// 4. Return object (or constructor's return if it's an object)
return (typeof result === 'object' && result !== null) ? result : obj;
}
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
// Using custom 'new'
const dog1 = myNew(Dog, 'Rex');
console.log(dog1.name); // 'Rex'
console.log(dog1.bark()); // "Rex says woof!"
console.log(dog1 instanceof Dog); // true
// Compare with real 'new'
const dog2 = new Dog('Max');
console.log(dog2.name); // 'Max'
console.log(dog2.bark()); // "Max says woof!"
// Constructor returning object overrides default behavior
function Special() {
this.x = 1;
return { y: 2 }; // Explicit object return
}
const s = new Special();
console.log(s.x); // undefined (returned object used instead)
console.log(s.y); // 2
// Constructor returning primitive doesn't override
function Normal() {
this.x = 1;
return 42; // Primitive ignored
}
const n = new Normal();
console.log(n.x); // 1 (this returned as usual)
Example: Constructor patterns and best practices
// Pattern 1: Instance properties + prototype methods (recommended)
function Car(make, model) {
this.make = make;
this.model = model;
}
Car.prototype.getDescription = function() {
return `${this.make} ${this.model}`;
};
// Pattern 2: Static methods (on constructor itself)
Car.fromString = function(str) {
const [make, model] = str.split(' ');
return new Car(make, model);
};
const car1 = new Car('Toyota', 'Camry');
const car2 = Car.fromString('Honda Civic');
// Pattern 3: Guard against missing 'new'
function SafeConstructor(value) {
// Check if called with 'new'
if (!(this instanceof SafeConstructor)) {
return new SafeConstructor(value);
}
this.value = value;
}
const obj1 = new SafeConstructor(10); // With 'new'
const obj2 = SafeConstructor(20); // Without 'new' - still works
console.log(obj1 instanceof SafeConstructor); // true
console.log(obj2 instanceof SafeConstructor); // true
// Pattern 4: Setting prototype after constructor definition
function Animal(name) {
this.name = name;
}
// Replace entire prototype (need to restore constructor)
Animal.prototype = {
constructor: Animal, // Restore constructor reference
speak() {
return `${this.name} makes a sound`;
},
eat() {
return `${this.name} is eating`;
}
};
const animal = new Animal('Lion');
console.log(animal.speak()); // "Lion makes a sound"
console.log(animal.constructor === Animal); // true (restored)
// ✗ Common mistake: adding methods as instance properties
function Inefficient(name) {
this.name = name;
// ✗ Bad: creates new function for each instance
this.greet = function() {
return `Hi, I'm ${this.name}`;
};
}
const i1 = new Inefficient('A');
const i2 = new Inefficient('B');
console.log(i1.greet === i2.greet); // false (different functions - wasteful)
Common Pitfalls:
- Forgetting 'new': Constructor called as regular function;
thisrefers to global object (or undefined in strict mode) - Methods as instance properties: Creating functions inside constructor wastes memory; use prototype instead
- Overwriting prototype: Breaks constructor reference; always restore
.constructorproperty - Modifying built-in prototypes: Never modify Object.prototype, Array.prototype, etc. (causes conflicts)
8.3 Object.create and Prototypal Inheritance
| Method | Syntax | Creates | Use Case |
|---|---|---|---|
| Object.create(proto) | Object.create(protoObj) |
New object with protoObj as prototype | Prototypal inheritance, object delegation |
| Object.create(proto, descriptors) | Object.create(proto, { props }) |
Object with prototype + property descriptors | Fine-grained property control |
| Object.create(null) | Object.create(null) |
Object with no prototype (null) | Pure data storage, avoid prototype pollution |
| Inheritance Pattern | Approach | Pros | Cons |
|---|---|---|---|
| Constructor Pattern | function + new + prototype | Familiar to class-based developers | Verbose, easy to forget 'new' |
| Object.create Pattern | Object.create(proto) | Simple, pure prototypal, no 'new' | Less familiar, no constructor function |
| ES6 Class | class syntax | Clean syntax, modern, standard | Syntactic sugar over prototypes |
| Factory Function | function returning object | Flexible, private state via closure | No shared prototype methods (unless manually added) |
Example: Object.create for prototypal inheritance
// Parent object (prototype)
const animal = {
type: 'animal',
eat() {
return `${this.name} is eating`;
},
sleep() {
return `${this.name} is sleeping`;
}
};
// Create child objects inheriting from parent
const dog = Object.create(animal);
dog.name = 'Rex';
dog.breed = 'Labrador';
dog.bark = function() {
return `${this.name} says woof!`;
};
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.breed = 'Persian';
cat.meow = function() {
return `${this.name} says meow!`;
};
// Access own properties
console.log(dog.name); // 'Rex' (own)
console.log(dog.breed); // 'Labrador' (own)
// Access inherited properties/methods
console.log(dog.type); // 'animal' (inherited)
console.log(dog.eat()); // "Rex is eating" (inherited method)
console.log(dog.bark()); // "Rex says woof!" (own method)
// Verify prototype chain
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(cat) === animal); // true
// Shared prototype
console.log(dog.eat === cat.eat); // true (same method from prototype)
// Modify prototype affects all children
animal.move = function() {
return `${this.name} is moving`;
};
console.log(dog.move()); // "Rex is moving" (newly added)
console.log(cat.move()); // "Whiskers is moving"
Example: Object.create with property descriptors
const parent = {
greet() {
return `Hello from ${this.name}`;
}
};
// Create object with prototype and property descriptors
const child = Object.create(parent, {
name: {
value: 'Child',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 10,
writable: true,
enumerable: true,
configurable: true
},
// Accessor property
description: {
get() {
return `${this.name}, age ${this.age}`;
},
enumerable: true,
configurable: true
}
});
console.log(child.name); // 'Child'
console.log(child.age); // 10
console.log(child.greet()); // "Hello from Child" (inherited)
console.log(child.description); // "Child, age 10" (computed)
// Object with null prototype (no inheritance)
const dict = Object.create(null);
dict.key1 = 'value1';
dict.key2 = 'value2';
console.log(dict.toString); // undefined (no Object.prototype)
console.log(dict.hasOwnProperty); // undefined
console.log(Object.getPrototypeOf(dict)); // null
// Useful for pure data storage
console.log('toString' in dict); // false (safe from prototype pollution)
Example: Implementing classical inheritance with Object.create
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name} is eating`;
};
Animal.prototype.sleep = function() {
return `${this.name} is sleeping`;
};
// Child constructor
function Dog(name, breed) {
// Call parent constructor
Animal.call(this, name); // Inherit instance properties
this.breed = breed;
}
// Set up prototype chain (critical step)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Restore constructor reference
// Add child-specific methods
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
// Create instance
const dog = new Dog('Rex', 'Labrador');
console.log(dog.name); // 'Rex' (from Animal)
console.log(dog.breed); // 'Labrador' (from Dog)
console.log(dog.eat()); // "Rex is eating" (inherited from Animal)
console.log(dog.bark()); // "Rex says woof!" (from Dog)
// Verify prototype chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog.constructor === Dog); // true
// Chain: dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
// ✗ Wrong way (don't do this)
// Dog.prototype = Animal.prototype; // Shares prototype (modifications affect both)
// Dog.prototype = new Animal(); // Creates unnecessary instance
// ✓ Correct way (ES6+ alternative)
class AnimalClass {
constructor(name) {
this.name = name;
}
eat() {
return `${this.name} is eating`;
}
}
class DogClass extends AnimalClass {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
return `${this.name} says woof!`;
}
}
const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.eat()); // "Max is eating"
console.log(modernDog.bark()); // "Max says woof!"
Best Practice: For inheritance, use ES6 classes (cleaner syntax). If using constructor
functions, always use
Object.create(Parent.prototype) to set up prototype chain, not
new Parent() or direct assignment. Always restore constructor property. Use
Object.create(null) for dictionary objects to avoid prototype pollution.
8.4 Prototype Methods and Property Override
| Concept | Description | Behavior |
|---|---|---|
| Method Inheritance | Child inherits methods from parent's prototype | Shared method executed with child instance as this |
| Method Override | Child defines method with same name as parent | Child method shadows parent method in prototype chain |
| super Pattern | Call parent method from overridden child method | Use Parent.prototype.method.call(this) or ES6 super |
| Property Shadowing | Own property hides prototype property with same name | Lookup stops at first match (own properties checked first) |
| Dynamic Dispatch | this binding determined at call time, not definition |
Enables polymorphism - same method name, different behavior |
| Pattern | Implementation | Use Case |
|---|---|---|
| Simple Override | Define method on child with same name | Completely replace parent behavior |
| Extend Parent Method | Call parent method + add child logic | Augment parent behavior |
| Delegate to Parent | Forward some calls to parent method | Conditional behavior based on state |
| Mixin Pattern | Copy methods from multiple sources | Multiple inheritance simulation |
Example: Method inheritance and override
// Parent with methods
const animal = {
speak() {
return `${this.name} makes a sound`;
},
eat() {
return `${this.name} is eating`;
}
};
// Child inherits and overrides
const dog = Object.create(animal);
dog.name = 'Rex';
// Inherited method (no override)
console.log(dog.eat()); // "Rex is eating" (from animal)
// Override speak method
dog.speak = function() {
return `${this.name} barks loudly`;
};
console.log(dog.speak()); // "Rex barks loudly" (overridden)
// Access parent method explicitly
console.log(animal.speak.call(dog)); // "Rex makes a sound" (parent method)
// Another child with different override
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.speak = function() {
return `${this.name} meows softly`;
};
console.log(cat.speak()); // "Whiskers meows softly" (different override)
// Polymorphism in action
const animals = [dog, cat];
animals.forEach(a => console.log(a.speak()));
// "Rex barks loudly"
// "Whiskers meows softly"
Example: Calling parent methods (super pattern)
// Constructor-based inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.describe = function() {
return `This is ${this.name}`;
};
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Override with extension - call parent method
Dog.prototype.describe = function() {
// Call parent method
const parentDesc = Animal.prototype.describe.call(this);
// Add child-specific info
return `${parentDesc}, a ${this.breed}`;
};
Dog.prototype.speak = function() {
// Completely override (no parent call)
return `${this.name} barks`;
};
const dog = new Dog('Rex', 'Labrador');
console.log(dog.describe()); // "This is Rex, a Labrador" (extended)
console.log(dog.speak()); // "Rex barks" (overridden)
// ES6 class with super keyword (cleaner)
class AnimalClass {
constructor(name) {
this.name = name;
}
describe() {
return `This is ${this.name}`;
}
speak() {
return `${this.name} makes a sound`;
}
}
class DogClass extends AnimalClass {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
describe() {
// Call parent method with super
return `${super.describe()}, a ${this.breed}`;
}
speak() {
// Override without calling parent
return `${this.name} barks`;
}
}
const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.describe()); // "This is Max, a Beagle"
console.log(modernDog.speak()); // "Max barks"
Example: Property shadowing and resolution
const parent = {
name: 'Parent',
value: 10,
getValue() {
return this.value;
}
};
const child = Object.create(parent);
console.log(child.name); // 'Parent' (inherited)
console.log(child.value); // 10 (inherited)
// Shadow property with own value
child.name = 'Child';
child.value = 20;
console.log(child.name); // 'Child' (own property shadows parent)
console.log(child.value); // 20 (own)
console.log(parent.name); // 'Parent' (parent unchanged)
// Method uses 'this.value' - gets child's own value
console.log(child.getValue()); // 20 (inherited method, child's value)
// Delete own property - parent property revealed
delete child.name;
console.log(child.name); // 'Parent' (parent property no longer shadowed)
// hasOwnProperty distinguishes own vs inherited
console.log(child.hasOwnProperty('value')); // true (own)
console.log(child.hasOwnProperty('getValue')); // false (inherited)
// Writing to inherited property creates own property
const obj = Object.create({ x: 1, y: 2 });
obj.x = 10; // Creates own property
console.log(obj.x); // 10 (own)
console.log(Object.getPrototypeOf(obj).x); // 1 (prototype unchanged)
// Arrays inherit push, pop, etc. from Array.prototype
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('push')); // false (inherited)
console.log(arr.hasOwnProperty('length')); // true (own property)
console.log(arr.hasOwnProperty(0)); // true (own property)
Example: Mixin pattern for method sharing
// Mixin: reusable method collections
const canEat = {
eat(food) {
return `${this.name} is eating ${food}`;
}
};
const canWalk = {
walk() {
return `${this.name} is walking`;
}
};
const canSwim = {
swim() {
return `${this.name} is swimming`;
}
};
// Mixin helper function
function mixin(target, ...sources) {
Object.assign(target, ...sources);
return target;
}
// Create object with multiple mixins
function Animal(name) {
this.name = name;
}
// Add mixins to prototype
mixin(Animal.prototype, canEat, canWalk);
const dog = new Animal('Rex');
console.log(dog.eat('bone')); // "Rex is eating bone"
console.log(dog.walk()); // "Rex is walking"
// console.log(dog.swim()); // Error: swim not available
// Different mix of capabilities
function Fish(name) {
this.name = name;
}
mixin(Fish.prototype, canEat, canSwim);
const fish = new Fish('Nemo');
console.log(fish.eat('algae')); // "Nemo is eating algae"
console.log(fish.swim()); // "Nemo is swimming"
// console.log(fish.walk()); // Error: walk not available
// ES6 class with mixins
const Flyable = {
fly() {
return `${this.name} is flying`;
}
};
class Bird {
constructor(name) {
this.name = name;
}
}
// Apply mixin to class
Object.assign(Bird.prototype, canEat, Flyable);
const bird = new Bird('Tweety');
console.log(bird.eat('seeds')); // "Tweety is eating seeds"
console.log(bird.fly()); // "Tweety is flying"
Best Practice: Use method override for polymorphism. Call parent methods with
super (ES6 classes) or Parent.prototype.method.call(this) (constructor functions). Use
mixins for horizontal code reuse. Remember: methods use this binding, so inherited methods access
instance properties correctly.
8.5 instanceof and Prototype Testing
| Method/Operator | Syntax | Returns | What It Checks |
|---|---|---|---|
| instanceof | obj instanceof Constructor |
boolean | Is Constructor.prototype in obj's prototype chain? |
| isPrototypeOf() | proto.isPrototypeOf(obj) |
boolean | Is proto in obj's prototype chain? |
| Object.getPrototypeOf() | Object.getPrototypeOf(obj) |
object or null | Returns obj's direct prototype |
| hasOwnProperty() | obj.hasOwnProperty(prop) |
boolean | Is prop an own property (not inherited)? |
| Object.hasOwn() ES2022 | Object.hasOwn(obj, prop) |
boolean | Safer alternative to hasOwnProperty |
| in operator | 'prop' in obj |
boolean | Is prop in obj or its prototype chain? |
| Test | Own Property | Inherited Property | Nonexistent |
|---|---|---|---|
| obj.prop | value | value | undefined |
| 'prop' in obj | true | true | false |
| obj.hasOwnProperty('prop') | true | false | false |
| Object.hasOwn(obj, 'prop') | true | false | false |
Example: instanceof operator
// Constructor-based type checking
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog = new Dog('Rex', 'Labrador');
// instanceof checks prototype chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true (all objects)
// Arrays
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
// Primitives
console.log('hello' instanceof String); // false (primitive, not object)
console.log(new String('hello') instanceof String); // true (wrapper object)
console.log(42 instanceof Number); // false
// instanceof can be fooled
function Fake() {}
Fake.prototype = dog; // Set prototype to instance
const fake = new Fake();
console.log(fake instanceof Dog); // true! (dog in prototype chain)
// Object.create
const parent = { type: 'parent' };
const child = Object.create(parent);
// No constructor function, instanceof doesn't work well
// console.log(child instanceof ???); // What to check?
Example: isPrototypeOf() method
const animal = {
eats: true
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
const longEaredRabbit = Object.create(rabbit);
longEaredRabbit.earLength = 10;
// Check prototype relationships
console.log(animal.isPrototypeOf(rabbit)); // true
console.log(animal.isPrototypeOf(longEaredRabbit)); // true (anywhere in chain)
console.log(rabbit.isPrototypeOf(longEaredRabbit)); // true
console.log(rabbit.isPrototypeOf(animal)); // false (wrong direction)
// Works with any object, not just constructors
console.log(Object.prototype.isPrototypeOf(rabbit)); // true (all objects)
console.log(Object.prototype.isPrototypeOf({})); // true
// Compare with instanceof (requires constructor)
function Animal() {}
const dog = new Animal();
console.log(dog instanceof Animal); // true
console.log(Animal.prototype.isPrototypeOf(dog)); // true (equivalent)
// null prototype
const noProto = Object.create(null);
console.log(Object.prototype.isPrototypeOf(noProto)); // false
Example: Property ownership testing
const parent = {
inherited: 'from parent',
sharedMethod() {
return 'shared';
}
};
const child = Object.create(parent);
child.own = 'own property';
// 'in' operator - checks own and inherited
console.log('own' in child); // true (own)
console.log('inherited' in child); // true (inherited)
console.log('sharedMethod' in child); // true (inherited)
console.log('nonexistent' in child); // false
// hasOwnProperty - checks only own properties
console.log(child.hasOwnProperty('own')); // true
console.log(child.hasOwnProperty('inherited')); // false (inherited)
console.log(child.hasOwnProperty('sharedMethod')); // false
// Object.hasOwn (ES2022) - safer alternative
console.log(Object.hasOwn(child, 'own')); // true
console.log(Object.hasOwn(child, 'inherited')); // false
// Why Object.hasOwn is safer
const obj = Object.create(null); // No prototype
obj.prop = 'value';
// obj.hasOwnProperty('prop'); // Error: hasOwnProperty not available
console.log(Object.hasOwn(obj, 'prop')); // true (works!)
// Distinguish own vs inherited in iteration
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(`Own: ${key}`);
} else {
console.log(`Inherited: ${key}`);
}
}
// Own: own
// Inherited: inherited
// Inherited: sharedMethod
// Object.keys() only returns own enumerable properties
console.log(Object.keys(child)); // ['own']
// Get all properties including inherited (manual)
function getAllProperties(obj) {
const props = new Set();
let current = obj;
while (current) {
Object.getOwnPropertyNames(current).forEach(p => props.add(p));
current = Object.getPrototypeOf(current);
}
return Array.from(props);
}
console.log(getAllProperties(child));
// Includes: own, inherited, sharedMethod, toString, hasOwnProperty, etc.
Example: Type checking best practices
// Checking built-in types
function checkType(value) {
// Arrays - use Array.isArray (most reliable)
if (Array.isArray(value)) {
return 'array';
}
// null (special case - typeof null === 'object')
if (value === null) {
return 'null';
}
// Primitives - use typeof
if (typeof value !== 'object') {
return typeof value; // 'string', 'number', 'boolean', etc.
}
// Objects - check constructor
if (value instanceof Date) return 'date';
if (value instanceof RegExp) return 'regexp';
if (value instanceof Map) return 'map';
if (value instanceof Set) return 'set';
return 'object';
}
console.log(checkType([1, 2, 3])); // 'array'
console.log(checkType('hello')); // 'string'
console.log(checkType(42)); // 'number'
console.log(checkType(null)); // 'null'
console.log(checkType(new Date())); // 'date'
console.log(checkType({ x: 1 })); // 'object'
// Duck typing - check for expected methods
function isIterable(obj) {
return obj != null && typeof obj[Symbol.iterator] === 'function';
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable('hello')); // true
console.log(isIterable({ x: 1 })); // false
console.log(isIterable(new Map())); // true
// Custom type guard (TypeScript-like pattern in JS)
function isDog(animal) {
return animal
&& typeof animal.bark === 'function'
&& typeof animal.breed === 'string';
}
const dog = { name: 'Rex', breed: 'Lab', bark() {} };
const cat = { name: 'Fluffy', meow() {} };
console.log(isDog(dog)); // true
console.log(isDog(cat)); // false
// Symbol.toStringTag for custom types
class CustomType {
get [Symbol.toStringTag]() {
return 'CustomType';
}
}
const custom = new CustomType();
console.log(Object.prototype.toString.call(custom)); // '[object CustomType]'
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(new Date())); // '[object Date]'
Common Pitfalls:
- instanceof with primitives: Returns false for primitives like
'string'even though they can use String methods - instanceof across frames: Different window/frame has different built-in constructors; use Array.isArray() for arrays
- hasOwnProperty safety: Objects with null prototype don't have hasOwnProperty; use Object.hasOwn() instead
- typeof null: Returns 'object' (JavaScript quirk); check
=== nullexplicitly
8.6 Object Delegation Patterns
| Pattern | Description | Key Concept |
|---|---|---|
| Behavior Delegation (OLOO) | Objects delegate to other objects (not classes) | Objects Linked to Other Objects - pure prototypal |
| Classical Inheritance | Parent-child hierarchy with constructors | Constructor functions + prototype chain |
| Concatenative Inheritance | Mix properties from multiple sources | Object.assign() / spread for composition |
| Functional Inheritance | Factory functions with closures for privacy | Functions return objects with private state |
| Aspect | Classical (Constructor) | Delegation (OLOO) |
|---|---|---|
| Syntax | new Constructor() |
Object.create(proto) |
| Focus | Parent-child classes | Peer objects delegating to each other |
| Initialization | Constructor function | Init method on prototype |
| Complexity | More verbose, mimics classes | Simpler, embraces prototypes |
| Mental Model | "This IS-A that" (inheritance) | "This HAS-A reference to that" (delegation) |
Example: OLOO pattern (Objects Linked to Other Objects)
// Behavior delegation style (OLOO)
const Vehicle = {
init(make, model) {
this.make = make;
this.model = model;
return this;
},
describe() {
return `${this.make} ${this.model}`;
}
};
const Car = Object.create(Vehicle);
Car.init = function(make, model, doors) {
// Delegate to Vehicle.init
Vehicle.init.call(this, make, model);
this.doors = doors;
return this;
};
Car.drive = function() {
return `Driving ${this.describe()}`;
};
// Create instances
const myCar = Object.create(Car).init('Toyota', 'Camry', 4);
const yourCar = Object.create(Car).init('Honda', 'Civic', 4);
console.log(myCar.describe()); // "Toyota Camry"
console.log(myCar.drive()); // "Driving Toyota Camry"
console.log(yourCar.drive()); // "Driving Honda Civic"
// No constructors, no 'new', just objects delegating
console.log(Object.getPrototypeOf(myCar) === Car); // true
console.log(Object.getPrototypeOf(Car) === Vehicle); // true
// Compare with constructor pattern
function VehicleConstructor(make, model) {
this.make = make;
this.model = model;
}
VehicleConstructor.prototype.describe = function() {
return `${this.make} ${this.model}`;
};
function CarConstructor(make, model, doors) {
VehicleConstructor.call(this, make, model);
this.doors = doors;
}
CarConstructor.prototype = Object.create(VehicleConstructor.prototype);
CarConstructor.prototype.constructor = CarConstructor;
CarConstructor.prototype.drive = function() {
return `Driving ${this.describe()}`;
};
const constructorCar = new CarConstructor('Ford', 'Focus', 4);
console.log(constructorCar.drive()); // Same result, more complex setup
Example: Concatenative inheritance (composition)
// Reusable behavior objects
const canEat = {
eat(food) {
return `${this.name} eats ${food}`;
}
};
const canWalk = {
walk() {
return `${this.name} walks`;
}
};
const canSwim = {
swim() {
return `${this.name} swims`;
}
};
// Compose objects by combining behaviors
function createDog(name) {
return {
name,
...canEat,
...canWalk,
bark() {
return `${this.name} barks`;
}
};
}
function createFish(name) {
return {
name,
...canEat,
...canSwim
};
}
function createDuck(name) {
return {
name,
...canEat,
...canWalk,
...canSwim,
quack() {
return `${this.name} quacks`;
}
};
}
const dog = createDog('Rex');
console.log(dog.eat('bone')); // "Rex eats bone"
console.log(dog.walk()); // "Rex walks"
console.log(dog.bark()); // "Rex barks"
// console.log(dog.swim()); // undefined (no swim capability)
const duck = createDuck('Donald');
console.log(duck.eat('bread')); // "Donald eats bread"
console.log(duck.walk()); // "Donald walks"
console.log(duck.swim()); // "Donald swims"
console.log(duck.quack()); // "Donald quacks"
// Advanced: functional mixin
const withLogging = (obj) => {
const originalMethods = {};
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'function') {
originalMethods[key] = obj[key];
obj[key] = function(...args) {
console.log(`Calling ${key} with`, args);
return originalMethods[key].apply(this, args);
};
}
});
return obj;
};
const loggedDog = withLogging(createDog('Max'));
loggedDog.eat('treat'); // Logs: "Calling eat with ['treat']"
// Returns: "Max eats treat"
Example: Functional inheritance with closures
// Factory function with private state
function createBankAccount(initialBalance) {
// Private variables (closure)
let balance = initialBalance;
let transactionHistory = [];
// Private function
function recordTransaction(type, amount) {
transactionHistory.push({
type,
amount,
timestamp: Date.now(),
balance
});
}
// Public interface
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
recordTransaction('deposit', amount);
}
return balance;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
recordTransaction('withdraw', amount);
}
return balance;
},
getBalance() {
return balance;
},
getHistory() {
// Return copy to prevent external modification
return [...transactionHistory];
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined (private!)
console.log(account.getHistory().length); // 2 transactions
// Inheritance with functional pattern
function createSavingsAccount(initialBalance, interestRate) {
// Get base account
const account = createBankAccount(initialBalance);
// Add new functionality
account.applyInterest = function() {
const interest = account.getBalance() * interestRate;
account.deposit(interest);
return account.getBalance();
};
return account;
}
const savings = createSavingsAccount(1000, 0.05);
savings.deposit(500); // 1500
savings.applyInterest(); // 1575 (5% interest)
console.log(savings.getBalance()); // 1575
Example: Practical delegation patterns
// Plugin system using delegation
const BaseApp = {
init(name) {
this.name = name;
this.plugins = [];
return this;
},
use(plugin) {
this.plugins.push(plugin);
if (plugin.install) {
plugin.install(this);
}
return this;
},
run() {
console.log(`${this.name} is running`);
this.plugins.forEach(p => p.onRun && p.onRun(this));
}
};
// Specific app type
const WebApp = Object.create(BaseApp);
WebApp.init = function(name, port) {
BaseApp.init.call(this, name);
this.port = port;
return this;
};
WebApp.start = function() {
console.log(`Server started on port ${this.port}`);
this.run();
};
// Plugins
const LoggerPlugin = {
install(app) {
console.log(`Logger installed in ${app.name}`);
},
onRun(app) {
console.log(`[LOG] ${app.name} executed`);
}
};
const CachePlugin = {
install(app) {
app.cache = new Map();
console.log(`Cache installed in ${app.name}`);
},
onRun(app) {
console.log(`[CACHE] Size: ${app.cache.size}`);
}
};
// Create app with plugins
const app = Object.create(WebApp)
.init('MyApp', 3000)
.use(LoggerPlugin)
.use(CachePlugin);
app.start();
// Logger installed in MyApp
// Cache installed in MyApp
// Server started on port 3000
// MyApp is running
// [LOG] MyApp executed
// [CACHE] Size: 0
Section 8 Summary
- Prototype chain: Property lookup traverses [[Prototype]] links from object to Object.prototype to null; use getPrototypeOf(), not __proto__
- Constructor pattern: Function with
newcreates instances; 4 steps: create, link prototype, execute, return; methods on prototype for efficiency - Object.create: Direct prototypal inheritance without constructors; clean delegation; use for pure prototypal patterns
- Method override: Child methods shadow parent; call parent with
superor Parent.prototype.method.call(this); enables polymorphism - Type testing: instanceof checks constructor chain; isPrototypeOf() checks object chain; Object.hasOwn() for property ownership (safer than hasOwnProperty)
- Delegation patterns: OLOO for pure delegation; concatenative for composition; functional for privacy; choose based on needs
9. Arrays and Array Methods Reference
9.1 Array Creation and Initialization Patterns
| Pattern | Syntax | Description | Example |
|---|---|---|---|
| Array Literal | [el1, el2, ...] |
Most common creation method; supports mixed types and sparse arrays | const arr = [1, 2, 3]; |
| Array Constructor | new Array(length) |
Creates array with specified length (holes); avoid for single numeric argument | new Array(3) // [empty × 3] |
| Array.of() | Array.of(...items) |
Creates array from arguments; fixes Array() single-number issue | Array.of(3) // [3] |
| Array.from() | Array.from(iterable, mapFn) |
Creates array from iterable/array-like; optional mapper function | Array.from('abc') // ['a','b','c'] |
| Spread Syntax | [...iterable] |
Shallow copy or convert iterable to array; modern and concise | [...set] // array from Set |
| Array.fill() | Array(n).fill(value) |
Create pre-filled array; value is shared reference for objects | Array(3).fill(0) // [0,0,0] |
| Array.keys() | Array.from(Array(n).keys()) |
Generate sequential number array from 0 to n-1 | [...Array(3).keys()] // [0,1,2] |
Example: Various array creation patterns
// Literal - most common
const fruits = ['apple', 'banana', 'orange'];
// Array.of - safe with single number
const single = Array.of(5); // [5] not [empty × 5]
// Array.from with mapper
const squares = Array.from({length: 5}, (_, i) => i ** 2);
// [0, 1, 4, 9, 16]
// From string
const chars = Array.from('hello'); // ['h','e','l','l','o']
// From Set
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
// Pre-fill with value
const zeros = Array(10).fill(0);
// Sequential numbers
const range = [...Array(5).keys()]; // [0, 1, 2, 3, 4]
const rangeFrom1 = Array.from({length: 5}, (_, i) => i + 1);
// [1, 2, 3, 4, 5]
// 2D array (careful with fill!)
const matrix = Array(3).fill(null).map(() => Array(3).fill(0));
Note: Avoid
new Array(n).fill([]) for 2D arrays as all rows share same reference.
Use .map(() => []) instead.
9.2 Array Mutating Methods (push, pop, splice, sort)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| push() | arr.push(el1, ...) |
Adds elements to end; modifies original array; O(1) time | New length |
| pop() | arr.pop() |
Removes and returns last element; O(1) time | Removed element |
| unshift() | arr.unshift(el1, ...) |
Adds elements to beginning; shifts indices; O(n) time | New length |
| shift() | arr.shift() |
Removes and returns first element; shifts indices; O(n) time | Removed element |
| splice() | arr.splice(start, deleteCount, ...items) |
Add/remove elements at any position; powerful but complex | Array of deleted elements |
| sort() | arr.sort(compareFn) |
Sorts in-place; default: string comparison; provide compareFn for numbers | Sorted array (same reference) |
| reverse() | arr.reverse() |
Reverses array in-place; O(n) time | Reversed array (same reference) |
| fill() | arr.fill(value, start, end) |
Fills with static value; optional range; O(n) time | Modified array |
| copyWithin() | arr.copyWithin(target, start, end) |
Shallow copies part of array to another location in same array | Modified array |
Example: Mutating array methods
const arr = [1, 2, 3];
// Stack operations (end)
arr.push(4, 5); // [1,2,3,4,5] returns 5
arr.pop(); // [1,2,3,4] returns 5
// Queue operations (beginning)
arr.unshift(0); // [0,1,2,3,4] returns 5
arr.shift(); // [1,2,3,4] returns 0
// splice - remove
const items = [1, 2, 3, 4, 5];
items.splice(2, 2); // [1,2,5] returns [3,4]
// splice - insert
items.splice(1, 0, 'a', 'b'); // [1,'a','b',2,5]
// splice - replace
items.splice(0, 2, 'x'); // ['x','b',2,5]
// sort - numbers (must provide compareFn!)
const nums = [10, 2, 5, 1];
nums.sort(); // [1, 10, 2, 5] ❌ string sort!
nums.sort((a, b) => a - b); // [1, 2, 5, 10] ✓
// sort - objects
const users = [{age: 30}, {age: 20}, {age: 25}];
users.sort((a, b) => a.age - b.age);
// reverse
[1, 2, 3].reverse(); // [3, 2, 1]
// fill
Array(5).fill(0); // [0,0,0,0,0]
[1,2,3,4,5].fill(0, 2, 4); // [1,2,0,0,5]
// copyWithin
[1,2,3,4,5].copyWithin(0, 3); // [4,5,3,4,5]
Warning: Default
sort() converts to strings! Always provide compare function for
numbers: arr.sort((a,b) => a - b)
9.3 Array Non-mutating Methods (slice, concat, join)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| slice() | arr.slice(start, end) |
Shallow copy of portion; negative indices from end; doesn't modify original | New array |
| concat() | arr.concat(arr2, ...) |
Merges arrays/values; shallow copy; doesn't modify originals | New array |
| join() | arr.join(separator) |
Converts to string with separator; default comma; doesn't modify | String |
| toString() | arr.toString() |
Converts to comma-separated string; same as join(',') |
String |
| toLocaleString() | arr.toLocaleString() |
Locale-aware string conversion; respects number/date formatting | String |
| flat() ES2019 | arr.flat(depth) |
Flattens nested arrays; default depth 1; Infinity for full flatten | New flattened array |
| flatMap() ES2019 | arr.flatMap(fn) |
Maps then flattens (depth 1); more efficient than map().flat() | New array |
| with() ES2023 | arr.with(index, value) |
Returns new array with element replaced; immutable alternative to arr[i]=val | New array |
| toReversed() ES2023 | arr.toReversed() |
Non-mutating reverse; returns new reversed array | New reversed array |
| toSorted() ES2023 | arr.toSorted(compareFn) |
Non-mutating sort; returns new sorted array | New sorted array |
| toSpliced() ES2023 | arr.toSpliced(start, deleteCount, ...items) |
Non-mutating splice; returns new array with changes | New array |
Example: Non-mutating array methods
const arr = [1, 2, 3, 4, 5];
// slice - extract portion
arr.slice(1, 3); // [2, 3]
arr.slice(-2); // [4, 5] (last 2)
arr.slice(); // [1,2,3,4,5] (shallow copy)
// concat - merge
[1, 2].concat([3, 4]); // [1, 2, 3, 4]
[1].concat(2, [3, 4], 5); // [1, 2, 3, 4, 5]
// join - to string
['a', 'b', 'c'].join(); // "a,b,c"
['a', 'b', 'c'].join(''); // "abc"
['a', 'b', 'c'].join(' - '); // "a - b - c"
// flat - nested arrays
[1, [2, 3], [4, [5]]].flat(); // [1, 2, 3, 4, [5]]
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// flatMap - map then flatten
['hello', 'world'].flatMap(s => s.split(''));
// ['h','e','l','l','o','w','o','r','l','d']
[1, 2, 3].flatMap(x => [x, x * 2]);
// [1, 2, 2, 4, 3, 6]
// ES2023 immutable methods
const original = [3, 1, 2];
original.toSorted(); // [1, 2, 3]
console.log(original); // [3, 1, 2] unchanged
original.toReversed(); // [2, 1, 3]
original.with(1, 99); // [3, 99, 2]
original.toSpliced(1, 1, 'a', 'b'); // [3, 'a', 'b', 2]
Note: Use spread
[...arr] or slice() for shallow copy. ES2023 adds
toSorted(), toReversed(), toSpliced(), with() for immutable
operations.
9.4 Array Iteration Methods (forEach, map, filter, reduce)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| forEach() | arr.forEach((el, i, arr) => {}) |
Execute function for each element; no return value; can't break/continue | undefined |
| map() | arr.map((el, i, arr) => {}) |
Transform each element; returns new array with results | New array |
| filter() | arr.filter((el, i, arr) => {}) |
Keep elements that pass test; returns new array with matches | New array |
| reduce() | arr.reduce((acc, el, i, arr) => {}, init) |
Reduce to single value; accumulator pattern; init value recommended | Any value |
| reduceRight() | arr.reduceRight((acc, el) => {}, init) |
Like reduce but right-to-left; useful for right-associative operations | Any value |
| flatMap() | arr.flatMap((el, i, arr) => {}) |
Map then flatten depth 1; efficient for map + flat combination | New flattened array |
| entries() | arr.entries() |
Returns iterator of [index, value] pairs | Iterator |
| keys() | arr.keys() |
Returns iterator of indices | Iterator |
| values() | arr.values() |
Returns iterator of values | Iterator |
Example: Array iteration methods
const nums = [1, 2, 3, 4, 5];
// forEach - side effects only
nums.forEach((n, i) => console.log(`${i}: ${n}`));
// map - transform
const doubled = nums.map(n => n * 2); // [2,4,6,8,10]
const objects = nums.map(n => ({value: n}));
// filter - select
const evens = nums.filter(n => n % 2 === 0); // [2,4]
const large = nums.filter(n => n > 3); // [4,5]
// reduce - accumulate
const sum = nums.reduce((acc, n) => acc + n, 0); // 15
const product = nums.reduce((acc, n) => acc * n, 1); // 120
// reduce - group by
const items = ['a', 'b', 'c', 'a', 'b'];
const counts = items.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {}); // {a: 2, b: 2, c: 1}
// reduce - flatten
[[1, 2], [3, 4], [5]].reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5] (use flat() instead!)
// Method chaining
nums
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0); // 12
// flatMap - map and flatten
['hello', 'world'].flatMap(w => w.split(''));
// ['h','e','l','l','o','w','o','r','l','d']
// Iterators
for (const [i, val] of nums.entries()) {
console.log(i, val); // 0 1, 1 2, ...
}
for (const i of nums.keys()) {
console.log(i); // 0, 1, 2, 3, 4
}
Note: Always provide initial value to
reduce() to avoid errors on empty arrays.
Use for...of if you need to break/continue; forEach() can't be stopped early.
9.5 Array Search Methods (find, includes, indexOf)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| find() | arr.find((el, i, arr) => {}) |
Returns first element that passes test; stops on first match | Element or undefined |
| findLast() ES2023 | arr.findLast((el, i, arr) => {}) |
Like find() but searches right-to-left; returns last match | Element or undefined |
| findIndex() | arr.findIndex((el, i, arr) => {}) |
Returns index of first element that passes test | Index or -1 |
| findLastIndex() ES2023 | arr.findLastIndex((el, i, arr) => {}) |
Like findIndex() but searches right-to-left | Index or -1 |
| includes() | arr.includes(value, fromIndex) |
Checks if array contains value; uses SameValueZero (handles NaN) | Boolean |
| indexOf() | arr.indexOf(value, fromIndex) |
Returns first index of value; uses strict equality (===); -1 if not found | Index or -1 |
| lastIndexOf() | arr.lastIndexOf(value, fromIndex) |
Returns last index of value; searches backwards | Index or -1 |
| at() ES2022 | arr.at(index) |
Returns element at index; negative indices from end; undefined if out of bounds | Element or undefined |
Example: Array search methods
const users = [
{id: 1, name: 'Alice', age: 25},
{id: 2, name: 'Bob', age: 30},
{id: 3, name: 'Charlie', age: 25}
];
// find - first match
const bob = users.find(u => u.name === 'Bob');
// {id: 2, name: 'Bob', age: 30}
const young = users.find(u => u.age < 30);
// {id: 1, name: 'Alice', age: 25}
// findLast - last match
const lastYoung = users.findLast(u => u.age === 25);
// {id: 3, name: 'Charlie', age: 25}
// findIndex
const bobIndex = users.findIndex(u => u.name === 'Bob'); // 1
const notFound = users.findIndex(u => u.name === 'Dave'); // -1
// includes - value check
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true ✓ (indexOf fails here)
// indexOf - strict equality
['a', 'b', 'c'].indexOf('b'); // 1
['a', 'b', 'c'].indexOf('d'); // -1
[1, 2, 3, 2].indexOf(2); // 1 (first occurrence)
// lastIndexOf - search backwards
[1, 2, 3, 2, 1].lastIndexOf(2); // 3
[1, 2, 3].lastIndexOf(4); // -1
// at - negative indexing
const arr = ['a', 'b', 'c', 'd'];
arr.at(0); // 'a'
arr.at(-1); // 'd' (last)
arr.at(-2); // 'c' (second to last)
arr.at(10); // undefined
// Comparison: includes vs indexOf
[1, 2, NaN].includes(NaN); // true
[1, 2, NaN].indexOf(NaN); // -1 ❌
// Check existence
if (users.find(u => u.id === 2)) {
// Found
}
// Get index and value
const index = users.findIndex(u => u.age === 25);
if (index !== -1) {
const user = users[index];
}
Note: Use
includes() for existence check (handles NaN); find() for
objects with conditions; at() for negative indexing (cleaner than
arr[arr.length - 1]).
9.6 Array Testing Methods (every, some)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| every() | arr.every((el, i, arr) => {}) |
Tests if ALL elements pass test; short-circuits on first false | Boolean |
| some() | arr.some((el, i, arr) => {}) |
Tests if ANY element passes test; short-circuits on first true | Boolean |
Example: Array testing methods
const nums = [2, 4, 6, 8, 10];
const mixed = [1, 2, 3, 4, 5];
// every - all must pass
nums.every(n => n % 2 === 0); // true (all even)
mixed.every(n => n % 2 === 0); // false
nums.every(n => n > 0); // true (all positive)
nums.every(n => n > 5); // false
// some - at least one must pass
mixed.some(n => n % 2 === 0); // true (has even)
mixed.some(n => n > 10); // false
[1, 3, 5].some(n => n % 2 === 0); // false (no even)
// Validation use cases
const users = [
{name: 'Alice', age: 25},
{name: 'Bob', age: 30},
{name: 'Charlie', age: 35}
];
// All adults?
const allAdults = users.every(u => u.age >= 18); // true
// Any seniors?
const hasSeniors = users.some(u => u.age >= 65); // false
// All have names?
const allNamed = users.every(u => u.name && u.name.length > 0);
// Any in 30s?
const hasThirties = users.some(u => u.age >= 30 && u.age < 40);
// Empty array edge cases
[].every(n => n > 100); // true (vacuous truth)
[].some(n => n > 100); // false
// Short-circuit behavior
const expensive = [1, 2, 3, 4, 5];
expensive.every(n => {
console.log('checking', n);
return n < 3;
}); // logs: checking 1, checking 2, checking 3
// stops at 3 (first failure)
expensive.some(n => {
console.log('checking', n);
return n > 3;
}); // logs: checking 1, 2, 3, 4
// stops at 4 (first success)
// Combine with other methods
const valid = users
.filter(u => u.age > 25)
.every(u => u.name.length > 0);
Note:
every() returns true for empty arrays (vacuous truth); some()
returns false. Both short-circuit, stopping iteration early for performance.
9.7 Array Destructuring and Spread Operations
| Pattern | Syntax | Description | Example |
|---|---|---|---|
| Basic Destructuring | const [a, b] = arr |
Extract elements into variables by position | const [x, y] = [1, 2] |
| Skip Elements | const [a, , c] = arr |
Omit elements with empty comma slots | const [, , z] = [1, 2, 3] |
| Rest Pattern | const [a, ...rest] = arr |
Collect remaining elements; must be last | const [h, ...t] = [1,2,3] |
| Default Values | const [a = 0] = arr |
Provide fallback if undefined; not for null | const [x = 10] = [] |
| Nested Destructuring | const [[a], [b]] = arr |
Destructure nested arrays recursively | const [[x]] = [[1]] |
| Swap Variables | [a, b] = [b, a] |
Elegant variable swap without temp | [x, y] = [y, x] |
| Spread in Array | [...arr] |
Shallow copy or expand iterable | [...arr1, ...arr2] |
| Spread in Call | fn(...arr) |
Pass array elements as separate arguments | Math.max(...nums) |
| Rest in Function | fn(...args) |
Collect function arguments into array | function sum(...n) |
Example: Destructuring and spread patterns
// Basic destructuring
const [a, b, c] = [1, 2, 3];
// a=1, b=2, c=3
// Skip elements
const [first, , third] = [1, 2, 3];
// first=1, third=3
// Rest pattern
const [head, ...tail] = [1, 2, 3, 4];
// head=1, tail=[2,3,4]
const [x, y, ...rest] = [1];
// x=1, y=undefined, rest=[]
// Default values
const [p = 0, q = 0] = [1];
// p=1, q=0
const [m = 'default'] = [undefined];
// m='default' (undefined triggers default)
const [n = 'default'] = [null];
// n=null (null doesn't trigger default!)
// Nested destructuring
const [[a1, a2], [b1, b2]] = [[1, 2], [3, 4]];
// a1=1, a2=2, b1=3, b2=4
// Swap variables
let i = 1, j = 2;
[i, j] = [j, i]; // i=2, j=1
// Function parameters
function sum([a, b]) {
return a + b;
}
sum([3, 4]); // 7
function coords({x, y, z = 0}) {
return [x, y, z];
}
// Spread - shallow copy
const original = [1, 2, 3];
const copy = [...original]; // [1, 2, 3]
// Spread - merge arrays
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2]; // [1,2,3,4]
const combined = [0, ...arr1, 2.5, ...arr2, 5];
// [0, 1, 2, 2.5, 3, 4, 5]
// Spread - function arguments
const nums = [5, 3, 8, 1];
Math.max(...nums); // 8 (instead of apply)
// Rest parameters
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
function first(a, b, ...rest) {
console.log(rest); // array of remaining args
}
// Convert iterable
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]
const str = 'hello';
const chars = [...str]; // ['h','e','l','l','o']
// Shallow copy warning
const nested = [[1, 2], [3, 4]];
const shallowCopy = [...nested];
shallowCopy[0][0] = 99;
console.log(nested[0][0]); // 99 ❌ (shared reference!)
Warning: Spread creates shallow copy. Nested objects/arrays
share references. Use
structuredClone() for deep copy.
9.8 Typed Arrays and Buffer Management
| Type | Bytes/Element | Range | Description |
|---|---|---|---|
| Int8Array | 1 | -128 to 127 | Signed 8-bit integer |
| Uint8Array | 1 | 0 to 255 | Unsigned 8-bit integer |
| Uint8ClampedArray | 1 | 0 to 255 (clamped) | Clamped unsigned 8-bit (for Canvas) |
| Int16Array | 2 | -32,768 to 32,767 | Signed 16-bit integer |
| Uint16Array | 2 | 0 to 65,535 | Unsigned 16-bit integer |
| Int32Array | 4 | -2³¹ to 2³¹-1 | Signed 32-bit integer |
| Uint32Array | 4 | 0 to 2³²-1 | Unsigned 32-bit integer |
| Float32Array | 4 | ±1.2×10⁻³⁸ to ±3.4×10³⁸ | 32-bit floating point |
| Float64Array | 8 | ±5.0×10⁻³²⁴ to ±1.8×10³⁰⁸ | 64-bit floating point |
| BigInt64Array | 8 | -2⁶³ to 2⁶³-1 | Signed 64-bit BigInt |
| BigUint64Array | 8 | 0 to 2⁶⁴-1 | Unsigned 64-bit BigInt |
| Buffer API | Syntax | Description | Use Case |
|---|---|---|---|
| ArrayBuffer | new ArrayBuffer(byteLength) |
Fixed-length raw binary data buffer; not directly accessible | Binary data storage |
| DataView | new DataView(buffer, offset, length) |
Low-level interface to read/write multiple types in buffer | Mixed-type binary data |
| SharedArrayBuffer | new SharedArrayBuffer(length) |
Shared memory buffer for Web Workers; requires COOP/COEP headers | Multi-threaded data sharing |
Example: Typed arrays and buffer operations
// Create typed arrays
const uint8 = new Uint8Array(4); // [0, 0, 0, 0]
const int32 = new Int32Array([1, 2, 3]); // [1, 2, 3]
// From ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 bytes
const view32 = new Int32Array(buffer); // 4 elements (16/4)
const view8 = new Uint8Array(buffer); // 16 elements
// Shared buffer - multiple views
view32[0] = 0x12345678;
console.log(view8[0]); // 0x78 (little-endian)
// Array-like operations (limited)
const arr = new Uint8Array([1, 2, 3, 4]);
arr.length; // 4
arr[0]; // 1
arr.slice(1, 3); // Uint8Array[2, 3]
arr.map(x => x * 2); // Uint8Array[2, 4, 6, 8]
// No push/pop/splice - fixed length!
// arr.push(5); ❌ TypeError
// Overflow behavior
const u8 = new Uint8Array(1);
u8[0] = 256; // wraps to 0
u8[0] = -1; // wraps to 255
const clamped = new Uint8ClampedArray(1);
clamped[0] = 256; // clamps to 255
clamped[0] = -1; // clamps to 0
// DataView - mixed types
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setInt8(0, 127); // byte 0: 127
view.setInt16(1, 1000); // bytes 1-2: 1000
view.setFloat32(4, 3.14); // bytes 4-7: 3.14
view.getInt8(0); // 127
view.getInt16(1); // 1000
view.getFloat32(4); // 3.14...
// Endianness control
view.setInt32(0, 0x12345678, true); // little-endian
view.setInt32(0, 0x12345678, false); // big-endian
// Convert regular array
const regular = [1, 2, 3, 4, 5];
const typed = new Int32Array(regular);
// Convert typed to regular
const back = Array.from(typed);
// or [...typed]
// Use cases
// 1. Binary file I/O
fetch('data.bin')
.then(r => r.arrayBuffer())
.then(buf => new Uint8Array(buf));
// 2. WebGL graphics
const vertices = new Float32Array([
-1.0, -1.0,
1.0, -1.0,
0.0, 1.0
]);
// 3. Image processing
const imageData = ctx.getImageData(0, 0, w, h);
const pixels = imageData.data; // Uint8ClampedArray
// RGBA: [r, g, b, a, r, g, b, a, ...]
// 4. Network protocols
const header = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
// SharedArrayBuffer (requires headers)
const shared = new SharedArrayBuffer(1024);
const worker1View = new Int32Array(shared);
const worker2View = new Int32Array(shared);
// Both workers see same memory
Note: Typed arrays are fixed-length, array-like objects for binary data. Use for
performance-critical operations (WebGL, image processing, binary I/O). Regular arrays support more methods and
dynamic sizing.
Section 9 Summary
- Creation: Use literals
[]for general use;Array.from()for iterables with mapper;Array.of()to avoid single-number trap; spread for copying - Mutating:
push/pop(end O(1)),shift/unshift(start O(n)),splice(any position);sortneeds compare function for numbers; these modify original - Non-mutating:
slice(copy),concat(merge),flat/flatMap(flatten); ES2023 addstoSorted/toReversed/toSpliced/withfor immutability - Iteration:
map(transform),filter(select),reduce(accumulate),forEach(side effects); always provide initial value to reduce - Search:
find/findIndex(with predicate),includes(existence, handles NaN),indexOf(position);at()for negative indexing - Testing:
every(all pass),some(any pass); both short-circuit;every([]) === true,some([]) === false - Destructuring:
[a, b] = arr, rest[...rest], skip with,, defaults; spread...arrfor copy/merge (shallow!) - Typed arrays: Fixed-length, type-specific, backed by ArrayBuffer; use for binary data, WebGL, performance; limited methods vs regular arrays
10. Strings and Template Literals
10.1 String Creation and String Literals
| Type | Syntax | Description | Example |
|---|---|---|---|
| Single Quotes | 'string' |
Basic string literal; escape ' as \' |
'Hello world' |
| Double Quotes | "string" |
Identical to single quotes; escape " as \" |
"Hello world" |
| Template Literal | `string` |
Backticks; supports interpolation, multiline, and tag functions | `Hello ${name}` |
| String Constructor | String(value) |
Convert to string; returns primitive (without new) |
String(123) // "123" |
| String Object | new String(value) |
Creates String object (wrapper); avoid - use primitives | new String("hi") |
| Escape Sequences | \n \t \\ \' \" |
Special characters: newline, tab, backslash, quotes | 'Line 1\nLine 2' |
| Unicode Escape | \uXXXX |
4-digit hex Unicode code point (BMP only) | '\u0041' // "A" |
| Unicode Code Point | \u{X...} |
1-6 hex digits; supports full Unicode range (including astral) | '\u{1F600}' // "😀" |
| Hex Escape | \xXX |
2-digit hex byte value (0x00-0xFF) | '\x41' // "A" |
| Raw String ES2015 | String.raw`...` |
Template literal without escape processing | String.raw`\n` // "\\n" |
Example: String creation patterns
// Literal syntax (preferred)
const single = 'Hello';
const double = "World";
const template = `Hello ${name}`;
// String constructor (type conversion)
String(123); // "123"
String(true); // "true"
String(null); // "null"
String({a: 1}); // "[object Object]"
// Avoid String object (wrapper)
const bad = new String("text"); // typeof bad === "object" ❌
const good = "text"; // typeof good === "string" ✓
// Escape sequences
'Line 1\nLine 2'; // newline
'Tab\tseparated'; // tab
'She said "hi"'; // double quotes in single
"It's okay"; // single quote in double
'Can\'t escape'; // escaped single quote
// More escape sequences
'C:\\Users\\name'; // backslashes
'First\x20Second'; // space using hex
'\u00A9 2025'; // © 2025
'\u{1F4A9}'; // 💩 (requires code point syntax)
// Multiline strings
const multi = `Line 1
Line 2
Line 3`; // preserves newlines
const legacy = 'Line 1\n' +
'Line 2\n' +
'Line 3'; // old way
// Raw strings (no escape processing)
String.raw`C:\Users\name`; // "C:\\Users\\name"
String.raw`\n\t`; // "\\n\\t" (literal backslashes)
// Empty and whitespace
const empty = '';
const space = ' ';
const whitespace = ' \t\n ';
// Immutability
const str = 'hello';
str[0] = 'H'; // No effect - strings are immutable
str.toUpperCase(); // Returns "HELLO", str unchanged
Note: Strings are immutable primitives. Use single/double quotes
for simple strings; template literals for interpolation/multiline. Avoid
new String() wrapper
objects.
10.2 String Methods and String Manipulation
| Method | Syntax | Description | Returns |
|---|---|---|---|
| length | str.length |
Number of UTF-16 code units (not characters!); astral chars count as 2 | Number |
| charAt() | str.charAt(index) |
Character at index; returns empty string if out of bounds | String |
| charCodeAt() | str.charCodeAt(index) |
UTF-16 code unit (0-65535); NaN if out of bounds | Number |
| codePointAt() | str.codePointAt(index) |
Full Unicode code point; handles astral characters correctly | Number |
| at() ES2022 | str.at(index) |
Character at index; negative indices from end; undefined if out of bounds | String | undefined |
| concat() | str.concat(str2, ...) |
Join strings; prefer + or template literals for readability |
String |
| slice() | str.slice(start, end) |
Extract substring; negative indices from end; preferred method | String |
| substring() | str.substring(start, end) |
Like slice but swaps args if start > end; no negative indices | String |
| substr() DEPRECATED | str.substr(start, length) |
Extract by length; deprecated - use slice() instead | String |
| toUpperCase() | str.toUpperCase() |
Convert to uppercase; locale-independent | String |
| toLowerCase() | str.toLowerCase() |
Convert to lowercase; locale-independent | String |
| toLocaleUpperCase() | str.toLocaleUpperCase(locale) |
Locale-aware uppercase (e.g., Turkish İ/I) | String |
| toLocaleLowerCase() | str.toLocaleLowerCase(locale) |
Locale-aware lowercase | String |
| trim() | str.trim() |
Remove whitespace from both ends | String |
| trimStart() / trimLeft() | str.trimStart() |
Remove whitespace from start only | String |
| trimEnd() / trimRight() | str.trimEnd() |
Remove whitespace from end only | String |
| padStart() ES2017 | str.padStart(length, padStr) |
Pad to length from start; default space | String |
| padEnd() ES2017 | str.padEnd(length, padStr) |
Pad to length from end; default space | String |
| repeat() | str.repeat(count) |
Repeat string count times; count must be non-negative integer | String |
Example: String manipulation methods
const str = 'Hello World';
// Access characters
str.charAt(0); // "H"
str[0]; // "H" (bracket notation)
str.at(0); // "H"
str.at(-1); // "d" (last character)
str.at(-2); // "l" (second to last)
// Character codes
str.charCodeAt(0); // 72 (UTF-16 code unit)
str.codePointAt(0); // 72 (Unicode code point)
// Emoji handling
const emoji = '😀';
emoji.length; // 2 ❌ (surrogate pair)
emoji.codePointAt(0); // 128512 ✓
emoji.charCodeAt(0); // 55357 ❌ (high surrogate only)
// Substrings
str.slice(0, 5); // "Hello"
str.slice(6); // "World"
str.slice(-5); // "World" (last 5)
str.slice(-5, -1); // "Worl"
str.substring(0, 5); // "Hello" (same as slice)
str.substring(5, 0); // "Hello" (swaps args!)
// Case conversion
'JavaScript'.toUpperCase(); // "JAVASCRIPT"
'JavaScript'.toLowerCase(); // "javascript"
// Turkish locale example
'i'.toLocaleUpperCase('tr'); // "İ" (dotted capital I)
'I'.toLocaleLowerCase('tr'); // "ı" (dotless small i)
// Whitespace removal
' hello '.trim(); // "hello"
' hello '.trimStart(); // "hello "
' hello '.trimEnd(); // " hello"
// Padding
'5'.padStart(3, '0'); // "005"
'123'.padStart(5, '0'); // "00123"
'hello'.padEnd(10, '.'); // "hello....."
const cardNum = '1234';
cardNum.padStart(16, '*'); // "************1234"
// Repeat
'*'.repeat(10); // "**********"
'abc'.repeat(3); // "abcabcabc"
// Concatenation
'Hello'.concat(' ', 'World'); // "Hello World"
'a' + 'b' + 'c'; // "abc" (preferred)
// Chaining
const result = ' HELLO '
.trim()
.toLowerCase()
.repeat(2)
.padStart(15, '.');
// "...hellohello"
Note: Use
slice() over substring() for consistency with arrays. Avoid
deprecated substr(). Remember length counts UTF-16 code units, not characters.
10.3 Template Literals and Tag Functions
| Feature | Syntax | Description | Example |
|---|---|---|---|
| Template Literal | `text` |
Backticks; supports interpolation and multiline | `Hello ${name}` |
| Interpolation | ${expression} |
Embed any expression; converted to string via ToString | `Sum: ${a + b}` |
| Multiline | `line1\nline2` |
Preserves actual newlines and indentation | `Line 1\nLine 2` |
| Tagged Template | tag`template` |
Function processes template; receives strings and values separately | html`<div>${text}</div>` |
| String.raw | String.raw`text` |
Built-in tag that returns raw string (no escape processing) | String.raw`\n` // "\\n" |
| Tag Function Signature | Receives strings array and interpolated values | ||
|---|---|---|---|
| Parameters | function tag(strings, ...values) |
||
| strings | Array of string literals; has .raw property for unprocessed strings |
||
| ...values | Rest parameter with all interpolated expression results | ||
Example: Template literals and tag functions
// Basic interpolation
const name = 'Alice';
const age = 25;
const msg = `Hello ${name}, you are ${age} years old`;
// "Hello Alice, you are 25 years old"
// Expressions
const a = 10, b = 20;
`Sum: ${a + b}`; // "Sum: 30"
`Comparison: ${a > b ? 'a' : 'b'}`; // "Comparison: b"
`Array: ${[1, 2, 3].join(', ')}`; // "Array: 1, 2, 3"
// Multiline
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`; // Preserves indentation and newlines
// Nesting
`Outer ${`Inner ${1 + 1}`} text`; // "Outer Inner 2 text"
// String.raw - no escape processing
String.raw`C:\Users\name`; // "C:\\Users\\name"
String.raw`Line 1\nLine 2`; // "Line 1\\nLine 2"
// Custom tag function - basic
function upper(strings, ...values) {
let result = '';
strings.forEach((str, i) => {
result += str;
if (i < values.length) {
result += String(values[i]).toUpperCase();
}
});
return result;
}
upper`Hello ${name}, age ${age}`;
// "Hello ALICE, age 25"
// Tag function - HTML escaping
function html(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] || '';
const escaped = String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
return result + str + escaped;
}, '');
}
const userInput = '<script>alert("xss")</script>';
html`<div>${userInput}</div>`;
// "<div><script>alert("xss")</script></div>"
// Tag function - SQL (parameterized queries)
function sql(strings, ...values) {
return {
query: strings.join('?'),
params: values
};
}
const userId = 123;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// {query: "SELECT * FROM users WHERE id = ?", params: [123]}
// Tag function - i18n translation
function t(strings, ...values) {
const key = strings.join('{}');
// Look up translation for key
return translate(key, values);
}
t`Welcome ${name}!`; // Translates "Welcome {}!" with name
// Access raw strings
function logger(strings, ...values) {
console.log('Processed:', strings);
console.log('Raw:', strings.raw);
console.log('Values:', values);
}
logger`Line 1\nLine 2`;
// Processed: ["Line 1
// Line 2"]
// Raw: ["Line 1\\nLine 2"]
// Values: []
// Empty interpolations
`a${null}b`; // "anullb"
`a${undefined}b`; // "aundefinedb"
`a${''}b`; // "ab"
Note: Tagged templates enable custom string processing (HTML escaping, i18n, SQL, styling). The
strings.raw property provides unprocessed strings. All interpolated values are converted to
strings.
10.4 String Interpolation and Expression Embedding
| Pattern | Syntax | Description | Example |
|---|---|---|---|
| Variable | ${variable} |
Insert variable value; converts to string | `Hello ${name}` |
| Expression | ${expr} |
Any valid expression; evaluated and converted to string | `Sum: ${a + b}` |
| Function Call | ${fn()} |
Call function and insert return value | `Time: ${getTime()}` |
| Ternary | ${cond ? a : b} |
Conditional expressions inline | `${online ? '🟢' : '🔴'}` |
| Logical AND | ${cond && value} |
Short-circuit conditional rendering; false becomes "false" string | `${hasError && errorMsg}` |
| Logical OR | ${val || default} |
Fallback value; beware of 0, '', false | `${name || 'Guest'}` |
| Nullish Coalescing | ${val ?? default} |
Fallback only for null/undefined; preserves 0, '', false | `${count ?? 0}` |
| Array Method | ${arr.method()} |
Array operations inline | `Items: ${arr.join(', ')}` |
| Object Property | ${obj.prop} |
Access nested properties | `Name: ${user.name}` |
| Nested Template | ${`inner ${val}`} |
Template literals can be nested | `Outer ${`inner ${x}`}` |
Example: String interpolation patterns
const user = {name: 'Alice', age: 25, online: true};
const items = ['apple', 'banana', 'orange'];
const count = 0;
// Basic variable
`Username: ${user.name}`; // "Username: Alice"
// Arithmetic expressions
`Total: ${(99.99 * 1.1).toFixed(2)}`; // "Total: $109.99"
`Progress: ${(completed / total * 100).toFixed(1)}%`;
// Function calls
`Generated at ${new Date().toISOString()}`;
`Random: ${Math.random().toFixed(4)}`;
// Ternary operator
`Status: ${user.online ? '🟢 Online' : '🔴 Offline'}`;
`You have ${count === 0 ? 'no' : count} items`;
// Logical AND (conditional content)
`${user.admin && 'Admin Panel'}`;
// If admin: "Admin Panel", else: "false" ❌
// Better: use ternary for boolean
`${user.admin ? 'Admin Panel' : ''}`;
// If admin: "Admin Panel", else: ""
// Logical OR (fallback)
`Hello ${user.nickname || user.name}`;
`Count: ${count || 'N/A'}`; // ❌ 0 becomes 'N/A'!
// Nullish coalescing (better fallback)
`Count: ${count ?? 'N/A'}`; // ✓ 0 stays 0
`Name: ${user.name ?? 'Anonymous'}`;
// Array methods
`Items: ${items.join(', ')}`; // "Items: apple, banana, orange"
`Count: ${items.length}`;
`First: ${items[0]}`;
// Array rendering with map
`<ul>${items.map(item => `<li>${item}</li>`).join('')}</ul>`;
// "<ul><li>apple</li><li>banana</li><li>orange</li></ul>"
// Object methods
`User: ${JSON.stringify(user)}`;
`Keys: ${Object.keys(user).join(', ')}`;
// Nested templates
const className = `btn ${user.online ? 'btn-success' : 'btn-danger'}`;
`<button class="${className}">${user.name}</button>`;
// Complex expressions
const price = 99.99;
const discount = 0.15;
const tax = 0.08;
`Final: ${(price * (1 - discount) * (1 + tax)).toFixed(2)}`;
// Method chaining
`Result: ${str.trim().toLowerCase().split(' ').join('-')}`;
// Type coercion
`Number: ${123}`; // "Number: 123"
`Boolean: ${true}`; // "Boolean: true"
`Null: ${null}`; // "Null: null"
`Undefined: ${undefined}`; // "Undefined: undefined"
`Object: ${{}}`; // "Object: [object Object]"
`Array: ${[1, 2]}`; // "Array: 1,2"
// Multi-line with interpolation
const html = `
<div class="user-card ${user.online ? 'online' : 'offline'}">
<h2>${user.name}</h2>
<p>Age: ${user.age}</p>
<p>Status: ${user.online ? '🟢' : '🔴'}</p>
</div>
`;
// Escaping interpolation
const literal = String.raw`Not interpolated: \${variable}`;
// "Not interpolated: ${variable}"
Warning: Logical AND
${cond && val} converts false to "false" string. Use ternary
${cond ? val : ''} for conditional rendering. Prefer ?? over || for
fallbacks.
10.5 Unicode Handling and Character Encoding
| Method/Feature | Syntax | Description | Example |
|---|---|---|---|
| length Property | str.length |
UTF-16 code units, not characters; astral symbols count as 2 | '😀'.length // 2 |
| Code Point Escape | \u{codepoint} |
1-6 hex digits; supports full Unicode (0-10FFFF) | '\u{1F600}' // 😀 |
| BMP Escape | \uXXXX |
4 hex digits; Basic Multilingual Plane only (0-FFFF) | '\u00A9' // © |
| String.fromCharCode() | String.fromCharCode(code, ...) |
Create from UTF-16 code units; astral chars need two codes (surrogate pair) | String.fromCharCode(65) // "A" |
| String.fromCodePoint() | String.fromCodePoint(point, ...) |
Create from Unicode code points; handles astral chars correctly | String.fromCodePoint(0x1F600) // "😀" |
| codePointAt() | str.codePointAt(index) |
Get full Unicode code point at position | '😀'.codePointAt(0) // 128512 |
| charCodeAt() | str.charCodeAt(index) |
Get UTF-16 code unit; only half of astral char (high surrogate) | '😀'.charCodeAt(0) // 55357 |
| normalize() | str.normalize(form) |
Unicode normalization: NFC, NFD, NFKC, NFKD; for comparison | 'café'.normalize('NFC') |
| localeCompare() | str.localeCompare(str2, locale) |
Locale-aware string comparison; handles accents, case correctly | 'ä'.localeCompare('z', 'de') |
| Spread/Array.from | [...str] |
Iterate by code points, not code units; handles emoji correctly | [...'😀🎉'].length // 2 |
| for...of Loop | for (const char of str) |
Iterates by Unicode code points; emoji-safe | for (const c of '😀') |
Example: Unicode handling
// BMP characters (single code unit)
const ascii = 'A';
ascii.length; // 1
ascii.charCodeAt(0); // 65
ascii.codePointAt(0); // 65
const copyright = '\u00A9'; // ©
copyright.length; // 1
// Astral plane (emoji, historic scripts)
const emoji = '😀';
emoji.length; // 2 ❌ (surrogate pair!)
emoji.charCodeAt(0); // 55357 (high surrogate)
emoji.charCodeAt(1); // 56832 (low surrogate)
emoji.codePointAt(0); // 128512 ✓ (full code point)
// Creating from code points
String.fromCharCode(65); // "A"
String.fromCharCode(0x00A9); // "©"
// Emoji requires fromCodePoint
String.fromCodePoint(0x1F600); // "😀"
String.fromCharCode(0x1F600); // ❌ Wrong!
// Surrogate pair manually (don't do this!)
String.fromCharCode(0xD83D, 0xDE00); // "😀" (works but use fromCodePoint)
// Code point escape syntax
'\u{1F600}'; // "😀"
'\u{1F4A9}'; // "💩"
'\u{1F680}'; // "🚀"
// Length issues with emoji
'hello'.length; // 5 ✓
'😀'.length; // 2 ❌
'👨👩👧👦'.length; // 11 ❌ (family emoji with ZWJ)
// Count actual characters
[...'hello'].length; // 5
[...'😀'].length; // 1 ✓
[...'😀🎉'].length; // 2 ✓
// However, combining characters still tricky
[...'👨👩👧👦'].length; // 7 (4 people + 3 ZWJ)
// Iterate correctly
for (const char of '😀🎉') {
console.log(char); // "😀", "🎉" (correct!)
}
// Split fails with emoji
'😀🎉'.split(''); // ['�','�','�','�'] ❌
[...'😀🎉']; // ['😀','🎉'] ✓
// Unicode normalization
const e1 = '\u00E9'; // é (single code point)
const e2 = '\u0065\u0301'; // é (e + combining acute accent)
e1 === e2; // false ❌
e1.length; // 1
e2.length; // 2
e1.normalize() === e2.normalize(); // true ✓
// Forms: NFC (composed), NFD (decomposed)
'café'.normalize('NFC').length; // 4
'café'.normalize('NFD').length; // 5 (e + combining accent)
// Locale-aware comparison
const strings = ['ä', 'z', 'a'];
strings.sort(); // ['a', 'z', 'ä'] ❌
strings.sort((a, b) => a.localeCompare(b, 'de'));
// ['a', 'ä', 'z'] ✓ (German collation)
'ä'.localeCompare('z', 'de'); // -1 (ä comes before z)
'ä'.localeCompare('z', 'sv'); // 1 (ä comes after z in Swedish)
// Case comparison with locale
'i'.toLocaleUpperCase('en'); // "I"
'i'.toLocaleUpperCase('tr'); // "İ" (dotted capital I in Turkish)
// Real character count (approximate)
function graphemeLength(str) {
return [...new Intl.Segmenter().segment(str)].length;
}
graphemeLength('😀'); // 1
graphemeLength('👨👩👧👦'); // 1 (treats family as one grapheme)
Warning:
length counts UTF-16 code units, not characters. Use
[...str].length for code point count. For grapheme clusters (with ZWJ, combining marks), use
Intl.Segmenter.
10.6 String Regular Expression Integration
| Method | Syntax | Description | Returns |
|---|---|---|---|
| match() | str.match(regexp) |
Find matches; with /g returns all matches, without returns match details | Array | null |
| matchAll() ES2020 | str.matchAll(regexp) |
Iterator of all matches with capture groups; regexp must have /g flag | Iterator |
| search() | str.search(regexp) |
Find index of first match; ignores /g flag | Index | -1 |
| replace() | str.replace(regexp|str, newStr|fn) |
Replace matches; without /g replaces first only | String |
| replaceAll() ES2021 | str.replaceAll(str|regexp, newStr|fn) |
Replace all occurrences; regexp must have /g or use string | String |
| split() | str.split(separator, limit) |
Split by string or regexp; optional limit on array size | Array |
| startsWith() | str.startsWith(search, pos) |
Check if starts with string; optional position; no regexp | Boolean |
| endsWith() | str.endsWith(search, length) |
Check if ends with string; optional length limit; no regexp | Boolean |
| includes() | str.includes(search, pos) |
Check if contains string; optional start position; no regexp | Boolean |
| indexOf() | str.indexOf(search, pos) |
Find first occurrence; case-sensitive; no regexp | Index | -1 |
| lastIndexOf() | str.lastIndexOf(search, pos) |
Find last occurrence; searches backwards; no regexp | Index | -1 |
Example: String RegExp integration
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;month>/
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;day>/
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
Note: Use
match() for simple searches; matchAll() for all matches
with details; replace() with functions for complex transformations.
includes/startsWith/endsWith don't support regexp.
Section 10 Summary
- Creation: Use single/double quotes for simple strings; template literals
`...`for interpolation/multiline; escape sequences\n \t; Unicode\u{...}for full range - Methods:
slice()for substrings (supports negative indices);trim/padStart/padEndfor formatting;toUpperCase/toLowerCasefor case; all return new strings (immutable) - Template literals:
`${expr}`interpolation; multiline preserves formatting; tagged templatestag`...`for custom processing (HTML escape, i18n, SQL) - Interpolation: Any expression; use ternary
? :not&&for conditionals;??for null/undefined fallback; beware||with 0/false - Unicode:
lengthcounts UTF-16 units not characters;[...str]for code points;codePointAt/fromCodePointfor emoji;normalize()for comparison - RegExp:
match/matchAllfind patterns;replace/replaceAllmodify;splitdivides;includes/startsWith/endsWithcheck (no regexp);searchfinds index
11. Regular Expressions and Pattern Matching
11.1 RegExp Literal and Constructor Syntax
| Syntax | Form | Description | Example |
|---|---|---|---|
| Literal Syntax | /pattern/flags |
Most common; compiled at parse time; cannot use variables in pattern | /hello/i |
| Constructor | new RegExp(pattern, flags) |
Dynamic patterns; compiled at runtime; pattern is string (escape backslashes!) | new RegExp('\\d+', 'g') |
| Constructor (no new) | RegExp(pattern, flags) |
Same as with new; returns RegExp instance |
RegExp('test') |
| From RegExp | new RegExp(regexp, flags) |
Clone with optional new flags; ES2015+ | new RegExp(/test/i, 'g') |
| Property | Type | Description | Example |
|---|---|---|---|
| source | String | Pattern text (without delimiters and flags) | /abc/i.source // "abc" |
| flags | String | All flags as string; alphabetically sorted | /a/gi.flags // "gi" |
| global | Boolean | Has g flag |
/a/g.global // true |
| ignoreCase | Boolean | Has i flag |
/a/i.ignoreCase // true |
| multiline | Boolean | Has m flag |
/a/m.multiline // true |
| dotAll | Boolean | Has s flag (. matches newlines) |
/a/s.dotAll // true |
| unicode | Boolean | Has u flag |
/a/u.unicode // true |
| sticky | Boolean | Has y flag |
/a/y.sticky // true |
| lastIndex | Number | Start position for next match; used with g or y flags |
regex.lastIndex = 0 |
Example: RegExp creation and properties
// Literal syntax (preferred)
const literal = /hello/i;
const pattern = /\d{3}-\d{4}/;
// Constructor - dynamic patterns
const word = 'test';
const dynamic = new RegExp(word, 'gi');
// Matches 'test' case-insensitively, globally
// Constructor - escape backslashes!
const wrong = new RegExp('\d+'); // ❌ \d becomes 'd'
const correct = new RegExp('\\d+'); // ✓ \\d becomes \d
// Or use String.raw
const regex = new RegExp(String.raw`\d+`);
// Clone with new flags
const original = /test/i;
const clone = new RegExp(original, 'g');
// Pattern: "test", flags: "g" (not "gi")
// Properties
const re = /hello/gi;
re.source; // "hello"
re.flags; // "gi"
re.global; // true
re.ignoreCase; // true
re.multiline; // false
// lastIndex (stateful with g or y flag)
const globalRe = /test/g;
globalRe.lastIndex; // 0 (initial)
globalRe.exec('test test');
globalRe.lastIndex; // 4 (after first match)
// Reset lastIndex
globalRe.lastIndex = 0;
// Create from string (user input)
function createRegex(userPattern, options = {}) {
const flags =
(options.global ? 'g' : '') +
(options.ignoreCase ? 'i' : '') +
(options.multiline ? 'm' : '');
try {
return new RegExp(userPattern, flags);
} catch (e) {
console.error('Invalid regex:', e.message);
return null;
}
}
// Escape special characters for literal matching
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\
// Literal syntax (preferred)
const literal = /hello/i;
const pattern = /\d{3}-\d{4}/;
// Constructor - dynamic patterns
const word = 'test';
const dynamic = new RegExp(word, 'gi');
// Matches 'test' case-insensitively, globally
// Constructor - escape backslashes!
const wrong = new RegExp('\d+'); // ❌ \d becomes 'd'
const correct = new RegExp('\\d+'); // ✓ \\d becomes \d
// Or use String.raw
const regex = new RegExp(String.raw`\d+`);
// Clone with new flags
const original = /test/i;
const clone = new RegExp(original, 'g');
// Pattern: "test", flags: "g" (not "gi")
// Properties
const re = /hello/gi;
re.source; // "hello"
re.flags; // "gi"
re.global; // true
re.ignoreCase; // true
re.multiline; // false
// lastIndex (stateful with g or y flag)
const globalRe = /test/g;
globalRe.lastIndex; // 0 (initial)
globalRe.exec('test test');
globalRe.lastIndex; // 4 (after first match)
// Reset lastIndex
globalRe.lastIndex = 0;
// Create from string (user input)
function createRegex(userPattern, options = {}) {
const flags =
(options.global ? 'g' : '') +
(options.ignoreCase ? 'i' : '') +
(options.multiline ? 'm' : '');
try {
return new RegExp(userPattern, flags);
} catch (e) {
console.error('Invalid regex:', e.message);
return null;
}
}
// Escape special characters for literal matching
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const userInput = 'hello.world';
const escaped = escapeRegex(userInput); // "hello\\.world"
const safe = new RegExp(escaped); // matches literal "hello.world"
#x26;");
}
const userInput = 'hello.world';
const escaped = escapeRegex(userInput); // "hello\\.world"
const safe = new RegExp(escaped); // matches literal "hello.world"
Note: Use literals
/pattern/flags for static patterns (better performance);
constructor for dynamic patterns. Remember to double-escape backslashes in strings: '\\d' not
'\d'.
11.2 RegExp Flags and Global Modifiers
| Flag | Name | Description | Effect |
|---|---|---|---|
| g | Global | Find all matches, not just first; affects exec(), test(), match(), replace() | Multiple matches |
| i | Ignore Case | Case-insensitive matching; [a-z] matches both cases | /a/i matches 'A' and 'a' |
| m | Multiline | ^ and $ match line boundaries, not just string start/end | ^word matches after \n |
| s ES2018 | DotAll | . (dot) matches newlines; normally dot excludes \n \r \u2028 \u2029 | /.+/s matches across lines |
| u ES2015 | Unicode | Full Unicode support; astral characters, Unicode escapes, properties | \u{1F600} and \p{...} |
| y ES2015 | Sticky | Match must start at lastIndex; no skipping ahead | For tokenizers/parsers |
| d ES2022 | Indices | Capture group indices in .indices property of match result | match.indices[0] |
Example: Flag behaviors
const str = 'Hello World\nHello Again';
// g - global (all matches)
str.match(/hello/i); // ["Hello"] (first only)
str.match(/hello/gi); // ["Hello", "Hello"] (all)
/\d+/g.test('123'); // true
/\d+/g.test('123'); // false (lastIndex moved!)
// i - ignore case
/hello/.test('HELLO'); // false
/hello/i.test('HELLO'); // true
// m - multiline (^ $ match line boundaries)
const multi = 'line1\nline2\nline3';
multi.match(/^line/g); // ["line"] (only first)
multi.match(/^line/gm); // ["line", "line", "line"] (each line)
/world$/.test('hello\nworld'); // true (end of string)
/world$/m.test('world\nhello'); // true (end of line)
// s - dotAll (. matches newlines)
/.+/.test('hello\nworld'); // false (. stops at \n)
/.+/s.test('hello\nworld'); // true (. matches \n)
'a\nb'.match(/a.b/); // null
'a\nb'.match(/a.b/s); // ["a\nb"]
// u - unicode (proper astral character handling)
'😀'.match(/./); // ["�"] ❌ (high surrogate only)
'😀'.match(/./u); // ["😀"] ✓
// Unicode property escapes (requires u flag)
/\p{Emoji}/u.test('😀'); // true
/\p{Script=Greek}/u.test('α'); // true
/\p{Letter}/u.test('A'); // true
// Count code points, not code units
'😀'.length; // 2
'😀'.match(/./gu); // ["😀"] (1 match)
'😀'.match(/./g); // ["�","�"] (2 matches)
// y - sticky (match at exact position)
const sticky = /\d+/y;
sticky.lastIndex = 0;
sticky.exec('123 456'); // ["123"]
sticky.lastIndex; // 3
sticky.exec('123 456'); // null (position 3 is space)
sticky.lastIndex; // 0 (reset on failure)
// Without y, would skip ahead and match 456
const notSticky = /\d+/g;
notSticky.lastIndex = 3;
notSticky.exec('123 456'); // ["456"]
// d - indices (capture group positions)
const indicesRe = /(\d+)-(\d+)/d;
const match = indicesRe.exec('id: 123-456');
match[1]; // "123"
match[2]; // "456"
match.indices[1]; // [4, 7] (position of "123")
match.indices[2]; // [8, 11] (position of "456")
// Combine flags
const combined = /pattern/gimsu;
combined.flags; // "gimsu" (alphabetically sorted)
Warning: Global
g flag makes RegExp stateful
(lastIndex). Reset with regex.lastIndex = 0 or use string methods. Always use u flag
for Unicode text!
11.3 Pattern Matching Methods (test, exec, match)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| test() | regex.test(str) |
Check if pattern matches; fastest for existence check | Boolean |
| exec() | regex.exec(str) |
Get match details with capture groups; use in loop with /g flag | Array | null |
| match() | str.match(regex) |
Without /g: like exec(); with /g: array of all matches (no details) | Array | null |
| matchAll() ES2020 | str.matchAll(regex) |
Iterator of all matches with full details; regex must have /g | Iterator |
Example: Pattern matching methods
const str = 'The year 2025 and 2026';
// test() - simple existence check
/\d+/.test(str); // true
/cat/.test(str); // false
// Use for validation
function isEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// exec() - detailed match info
const yearRe = /(\d{4})/;
const match = yearRe.exec(str);
match[0]; // "2025" (full match)
match[1]; // "2025" (capture group 1)
match.index; // 9 (position)
match.input; // original string
// exec() with /g flag (iterate)
const yearReGlobal = /(\d{4})/g;
let m;
while ((m = yearReGlobal.exec(str)) !== null) {
console.log(m[1], 'at', m.index);
// "2025" at 9
// "2026" at 18
}
// match() without /g (same as exec)
str.match(/(\d{4})/);
// ["2025", "2025", index: 9, input: "...", groups: undefined]
// match() with /g (all matches, no details)
str.match(/\d{4}/g); // ["2025", "2026"]
str.match(/\d+/g); // ["2025", "2026"]
// matchAll() - best of both (ES2020+)
const years = str.matchAll(/(\d{4})/g);
for (const match of years) {
console.log(match[1], 'at', match.index);
// "2025" at 9
// "2026" at 18
}
// Convert to array
const allMatches = [...str.matchAll(/(\d{4})/g)];
// Named capture groups
const phoneRe = /(?<area>\d{3})-(?<exchange>\d{3})-(?<number>\d{4})/;
const phone = '555-123-4567';
const phoneMatch = phone.match(phoneRe);
phoneMatch[1]; // "555"
phoneMatch.groups.area; // "555"
phoneMatch.groups.exchange; // "123"
phoneMatch.groups.number; // "4567"
// Multiple patterns
const email = 'user@example.com';
const emailRe = /^(?<user>[^@]+)@(?<domain>[^@]+)$/;
const emailMatch = email.match(emailRe);
emailMatch.groups.user; // "user"
emailMatch.groups.domain; // "example.com"
// No match returns null
'abc'.match(/\d+/); // null
/\d+/.test('abc'); // false
/\d+/.exec('abc'); // null
// Global test() gotcha (stateful!)
const re = /test/g;
re.test('test test'); // true (first call)
re.test('test test'); // true (second call)
re.test('test test'); // false! (lastIndex past end)
re.lastIndex; // 0 (reset)
// Solution: don't reuse global regex for test()
function hasDigit(str) {
return /\d/.test(str); // no /g, always works
}
// Or reset lastIndex
function hasPattern(str, regex) {
regex.lastIndex = 0;
return regex.test(str);
}
Note: Use
test() for boolean checks; match() for simple extraction;
matchAll() for all matches with details. Avoid test() with global regex in loops
(stateful).
11.4 String RegExp Methods (replace, split, search)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| replace() | str.replace(regex, newStr|fn) |
Replace first match (or all with /g); newStr can use $1, $2, etc. | String |
| replaceAll() ES2021 | str.replaceAll(str|regex, newStr|fn) |
Replace all occurrences; regex must have /g flag | String |
| split() | str.split(regex, limit) |
Split string by pattern; optional limit; capture groups included in result | Array |
| search() | str.search(regex) |
Find index of first match; ignores /g flag; like indexOf for regex | Index | -1 |
| Replacement Patterns (in replace newStr argument) | |||
|---|---|---|---|
| Pattern | Inserts | Example | Result |
| $$ | Literal $ | '$10'.replace(/\d+/, '$$&') |
"$&10" |
| $& | Entire match | 'cat'.replace(/\w+/, '[$&]') |
"[cat]" |
| $` | Text before match | 'a-b'.replace(/-/, '$`') |
"aab" |
| $' | Text after match | 'a-b'.replace(/-/, "$'") |
"abb" |
| $n | Capture group n (1-99) | 'John Doe'.replace(/(\w+) (\w+)/, '$2, $1') |
"Doe, John" |
| $<name> | Named capture group |
'2025-12-17'.replace(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, '$<m>/$<d>/$<y>')
|
"12/17/2025" |
Example: String RegExp methods
const str = 'Hello World, hello again';
// replace - first match
str.replace('hello', 'hi'); // "Hello World, hi again"
str.replace(/hello/, 'hi'); // "Hello World, hi again"
// replace - all matches (with /g)
str.replace(/hello/gi, 'hi'); // "hi World, hi again"
// replaceAll - simpler for strings
'aaa'.replace(/a/g, 'b'); // "bbb"
'aaa'.replaceAll('a', 'b'); // "bbb"
// Replacement patterns
'test'.replace(/test/, '[
const str = 'Hello World, hello again';
// replace - first match
str.replace('hello', 'hi'); // "Hello World, hi again"
str.replace(/hello/, 'hi'); // "Hello World, hi again"
// replace - all matches (with /g)
str.replace(/hello/gi, 'hi'); // "hi World, hi again"
// replaceAll - simpler for strings
'aaa'.replace(/a/g, 'b'); // "bbb"
'aaa'.replaceAll('a', 'b'); // "bbb"
// Replacement patterns
'test'.replace(/test/, '[$&]'); // "[test]"
'a-b'.replace(/-/, '($`|$\')'); // "a(a|b)b"
// Capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// Named groups
const date = '2025-12-17';
date.replace(
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// Replace with function
str.replace(/\b\w+\b/g, match => match.toUpperCase());
// "HELLO WORLD, HELLO AGAIN"
// Function receives: match, p1, p2, ..., offset, string, groups
'1 plus 2 equals 3'.replace(/(\d+)/g, (match, p1, offset) => {
return parseInt(p1) * 2;
}); // "2 plus 4 equals 6"
// Advanced replacements
const text = 'Price: $10.50, Total: $20.00';
text.replace(/\$(\d+\.\d+)/g, (match, price) => {
return `$${(parseFloat(price) * 1.1).toFixed(2)}`;
}); // "Price: $11.55, Total: $22.00"
// split - by regex
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split - with limit
'a-b-c-d'.split(/-/, 2); // ["a", "b"]
// split - capture groups included in result
'a1b2c'.split(/(\d)/);
// ["a", "1", "b", "2", "c"]
// split - whitespace
' hello world '.split(/\s+/);
// ["", "hello", "world", ""]
' hello world '.trim().split(/\s+/);
// ["hello", "world"]
// search - find index
str.search(/world/i); // 6
str.search(/cat/); // -1
// search vs indexOf
'hello'.indexOf('l'); // 2 (first 'l')
'hello'.search(/l/); // 2 (first 'l')
// search with patterns
'abc123xyz'.search(/\d/); // 3 (first digit)
'test@email.com'.search(/@/); // 4
// Use cases
// 1. Format phone numbers
function formatPhone(phone) {
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
formatPhone('5551234567'); // "(555) 123-4567"
// 2. Sanitize HTML
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. Slugify
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
slugify('Hello World!'); // "hello-world"
// 4. Extract data
const email = 'Contact: user@example.com';
const match = email.match(/[\w.]+@[\w.]+/);
const emailAddr = match ? match[0] : null;
#x26;]'); // "[test]"
'a-b'.replace(/-/, '(
1. JavaScript Syntax and Language Fundamentals
1.1 JavaScript Syntax Structure and Statements
Statement Type
Syntax
Description
Example
Expression Statement
expression;
Any valid expression followed by semicolon (ASI applies)
x = 5; 2 + 2;
Declaration Statement
var/let/const name;
Declares variables with specific scope and mutability
let x; const y = 10;
Block Statement
{ statements }
Groups multiple statements; creates block scope for let/const
{ let x = 1; }
Empty Statement
;
Does nothing; useful in loop bodies or as placeholder
while(condition);
Labeled Statement
label: statement
Adds identifier to statement for break/continue targeting
loop1: for(...) {...}
Function Declaration
function name() {}
Hoisted function definition, available before declaration
function add(a,b) { return a+b; }
Class Declaration
class Name {}
Declares class; not hoisted like functions
class User { constructor() {} }
Import Statement
import x from 'mod';
Loads module exports; hoisted and executed first
import { func } from './module';
Export Statement
export default x;
Exposes values/functions from module
export { func, var };
Example: Statement types in action
// Expression statements
x = 5;
y = x + 10;
// Block statement with local scope
{
let blockScoped = 'local';
const PI = 3.14159;
}
// Labeled statement for loop control
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outer;
}
}
ASI (Automatic Semicolon Insertion): JavaScript automatically inserts semicolons in certain
cases, but relying on it can cause bugs. Always use explicit semicolons for clarity.
1.2 Variable Declarations (var, let, const)
Declaration
Scope
Hoisting
Reassignment
Redeclaration
TDZ
var LEGACY
Function scope
Yes (undefined)
✓ Allowed
✓ Allowed
✗ No
let ES6
Block scope
Yes (uninitialized)
✓ Allowed
✗ SyntaxError
✓ Yes
const ES6
Block scope
Yes (uninitialized)
✗ TypeError
✗ SyntaxError
✓ Yes
Feature
var
let
const
Global Object Property
✓ Creates property on window/global
✗ Does not create property
✗ Does not create property
Loop Binding
Single binding (shared)
New binding per iteration
New binding per iteration
Initialization Required
✗ Optional
✗ Optional
✓ Required at declaration
Object/Array Mutation
Allowed
Allowed
Allowed (reference is const)
Example: Variable declaration differences
// var: function-scoped, hoisted
console.log(x); // undefined (hoisted)
var x = 5;
if (true) {
var x = 10; // Same variable
}
console.log(x); // 10
// let: block-scoped, TDZ
// console.log(y); // ReferenceError: Cannot access before initialization
let y = 5;
if (true) {
let y = 10; // Different variable
console.log(y); // 10
}
console.log(y); // 5
// const: block-scoped, immutable binding
const z = 5;
// z = 10; // TypeError: Assignment to constant
const obj = { a: 1 };
obj.a = 2; // ✓ OK: mutation allowed
// obj = {}; // ✗ TypeError: reassignment not allowed
Warning: const creates an immutable binding, not an
immutable value. Object properties can still be modified.
1.3 Identifier Rules and Naming Conventions
Rule
Description
Valid Examples
Invalid Examples
First Character
Letter (a-z, A-Z), underscore (_), or dollar sign ($)
_var, $elem, myVar
1var, -name, @value
Subsequent Characters
Letters, digits (0-9), underscore, dollar sign
var1, _temp2, $el3
my-var, my.var, my var
Case Sensitivity
JavaScript is case-sensitive; myVar ≠ myvar
myVar, MyVar, MYVAR
All are different variables
Unicode Support
Unicode letters allowed (international characters)
μ, café, 変数
Avoid for portability
Reserved Words
Cannot use JavaScript keywords as identifiers
myClass, _return
class, return, if
Convention
Usage
Example
Purpose
camelCase
Variables, functions, methods
myVariable, getUserData()
Standard for most identifiers
PascalCase
Classes, constructors, components
UserProfile, DataService
Distinguishes classes from functions
UPPER_SNAKE_CASE
Constants, configuration values
MAX_SIZE, API_URL
Indicates immutable values
_privateConvention
Private/internal members (convention)
_internalCache, _helper()
Signals internal use only
#privateFields ES2022
True private class fields
#privateValue
Language-level privacy
Example: Naming conventions in practice
// Constants
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
// Variables and functions (camelCase)
let userName = 'John';
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Classes (PascalCase)
class UserProfile {
#privateData; // True private field
constructor(name) {
this._internalId = Math.random(); // Convention-based private
this.#privateData = name;
}
}
// Boolean variables (descriptive prefix)
const isActive = true;
const hasPermission = false;
const canEdit = user.role === 'admin';
1.4 Comments and Documentation Syntax
Comment Type
Syntax
Usage
Example
Single-line
// comment
Brief explanations, inline notes
// Calculate sum
Multi-line
/* comment */
Longer explanations, block comments
/* This is a multi-line comment */
JSDoc Block
/** @tag */
API documentation, type annotations
/** @param {string} name */
Hashbang
#!/usr/bin/env node
Shebang for executable scripts (line 1 only)
Node.js CLI scripts
HTML-style LEGACY
<!-- comment -->
Legacy browser compatibility (avoid)
Only in embedded scripts
JSDoc Tag
Purpose
Syntax
Example
@param
Function parameter documentation
@param {type} name description
@param {number} age User's age
@returns
Return value description
@returns {type} description
@returns {boolean} True if valid
@typedef
Custom type definition
@typedef {Object} TypeName
@typedef {Object} User
@type
Variable type annotation
@type {type}
@type {string[]}
@deprecated
Mark as deprecated
@deprecated Use newFunc instead
Signals obsolete code
@example
Usage examples
@example functionCall()
Provides code samples
@throws
Documents exceptions
@throws {Error} description
@throws {TypeError}
Example: JSDoc documentation
/**
* Calculates the total price with tax
* @param {number} price - Base price of item
* @param {number} taxRate - Tax rate as decimal (e.g., 0.08)
* @returns {number} Total price including tax
* @throws {TypeError} If parameters are not numbers
* @example
* calculateTotal(100, 0.08); // Returns 108
*/
function calculateTotal(price, taxRate) {
if (typeof price !== 'number' || typeof taxRate !== 'number') {
throw new TypeError('Parameters must be numbers');
}
return price * (1 + taxRate);
}
/**
* @typedef {Object} User
* @property {string} name - User's full name
* @property {number} age - User's age
* @property {string[]} roles - User roles
*/
/** @type {User} */
const user = {
name: 'John Doe',
age: 30,
roles: ['admin', 'user']
};
1.5 Strict Mode and Its Effects
Aspect
Non-Strict Mode
Strict Mode
Impact
Activation
Default behavior
'use strict'; at file/function start
Enables stricter parsing and error handling
Implicit Globals
Creates global variable
ReferenceError
Prevents accidental globals from typos
Assignment to Non-writable
Silently fails
TypeError
Throws error on read-only property assignment
Delete Variables
Returns false
SyntaxError
Cannot delete variables, functions, or arguments
Duplicate Parameters
Allowed (last wins)
SyntaxError
Prevents function param name conflicts
Octal Literals
0123 allowed
SyntaxError
Use 0o123 instead
this in Functions
Global object (window)
undefined
Prevents accidental global modifications
with Statement
Allowed
SyntaxError
Banned due to scope ambiguity
eval Scope
Creates variables in surrounding scope
Own scope only
Prevents eval from polluting scope
Reserved Words
Some allowed as identifiers
Stricter: implements, interface, let, package, private, protected, public, static, yield
Future-proofs code for new keywords
Example: Strict mode effects
'use strict';
// 1. Prevents implicit globals
// mistypedVariable = 17; // ReferenceError (without strict: creates global)
// 2. Prevents duplicate parameters
// function sum(a, a, c) { } // SyntaxError (without strict: allowed)
// 3. Makes 'this' undefined in functions
function showThis() {
console.log(this); // undefined (without strict: window/global)
}
showThis();
// 4. Prevents assignment to non-writable properties
const obj = {};
Object.defineProperty(obj, 'x', { value: 42, writable: false });
// obj.x = 9; // TypeError (without strict: silently fails)
// 5. Prevents deleting variables
let x = 10;
// delete x; // SyntaxError (without strict: returns false)
// 6. Safer eval
eval('var y = 2;');
// console.log(y); // ReferenceError (without strict: y accessible)
// 7. Octal literals banned
// const octal = 0123; // SyntaxError
const octalCorrect = 0o123; // ✓ Use 0o prefix
Note: ES6 modules and classes automatically use strict mode. No need to add
'use strict'; explicitly.
1.6 JavaScript Reserved Words and Keywords
Category
Keywords
Description
Control Flow
if, else, switch, case, default, break, continue, return
Conditional execution and flow control
Loops
for, while, do, in, of
Iteration constructs
Declarations
var, let, const, function, class
Variable and function definitions
Exception Handling
try, catch, finally, throw
Error handling constructs
Object-Oriented
new, this, super, extends, static
OOP keywords
Type/Value
null, undefined, true, false, NaN, Infinity
Primitive values and special values
Operators
typeof, instanceof, void, delete, in
Special operators
Module System
import, export, from, as, default
ES6 module syntax
Async
async, await, yield
Asynchronous programming keywords
Other
debugger, with DEPRECATED
Debugging and legacy features
Future Reserved (Strict Mode)
Status
Notes
implements, interface, package
Reserved
Reserved for potential future use in strict mode
private, protected, public
Reserved
Access modifiers reserved for future features
enum
Reserved (all modes)
Reserved for enumeration type (not yet implemented)
arguments, eval
Restricted in strict mode
Cannot be used as identifiers or assigned to
Warning: While some keywords like undefined and NaN are not
technically reserved, they reference global properties and should not be used as variable names to avoid
confusion.
Section 1 Summary
- JavaScript uses various statement types for different purposes
(expressions, declarations, blocks)
- Use let/const over
var for block scoping and better error
detection
- Follow naming conventions: camelCase for variables/functions, PascalCase
for classes, UPPER_SNAKE_CASE for constants
- JSDoc comments provide type information and documentation for better IDE
support
- Strict mode catches common mistakes and prevents unsafe actions
(auto-enabled in modules/classes)
- Avoid using reserved keywords as identifiers; they're reserved for language
features
2. Data Types and Type System
2.1 Primitive Types (string, number, boolean, null, undefined, symbol, bigint)
Type
Description
typeof Result
Default Value
Wrapper Object
string
Immutable sequence of Unicode characters
"string"
"" (empty string)
String
number
64-bit IEEE 754 floating point (±1.8×10308)
"number"
0
Number
boolean
Logical true or false value
"boolean"
false
Boolean
null
Intentional absence of value (assignment)
"object" BUG
N/A
None
undefined
Variable declared but not assigned
"undefined"
undefined
None
symbol ES6
Unique, immutable identifier for object properties
"symbol"
N/A
Symbol
bigint ES2020
Arbitrary precision integers (beyond 253-1)
"bigint"
0n
BigInt
Type
Literal Syntax
Constructor
Special Values
string
'text', "text", `template`
String("value")
Empty string ""
number
42, 3.14, 1e5, 0xFF, 0o77, 0b1010
Number("123")
NaN, Infinity, -Infinity
boolean
true, false
Boolean(value)
Only true and false
null
null
N/A
Only null
undefined
undefined
undefined or void 0
Only undefined
symbol
N/A (must use function)
Symbol("desc")
Each symbol is unique
bigint
42n, 0xFFn, 0o77n, 0b1010n
BigInt("123")
No decimal values
Example: Primitive type characteristics
// Immutability: primitives are immutable
let str = "hello";
str[0] = "H"; // No effect, strings are immutable
console.log(str); // "hello"
// Primitive vs Wrapper auto-boxing
let num = 42;
console.log(num.toString()); // "42" - temporary wrapper created
// Special number values
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
// Symbols are unique
const sym1 = Symbol("desc");
const sym2 = Symbol("desc");
console.log(sym1 === sym2); // false (each is unique)
// BigInt for large integers
const bigNum = 9007199254740991n + 1n;
console.log(bigNum); // 9007199254740992n
Warning: typeof null returns "object" due to a legacy bug. Use
value === null for null checking.
2.2 Reference Types (object, array, function)
Type
Description
typeof Result
Mutable
Stored By
Object
Collection of key-value pairs (properties)
"object"
✓ Yes
Reference
Array
Ordered collection with numeric indices (subtype of Object)
"object"
✓ Yes
Reference
Function
Callable object with executable code
"function"
✓ Yes (properties)
Reference
Date
Represents date and time (milliseconds since epoch)
"object"
✓ Yes
Reference
RegExp
Regular expression pattern matcher
"object"
✓ Yes
Reference
Map ES6
Key-value pairs with any key type
"object"
✓ Yes
Reference
Set ES6
Collection of unique values
"object"
✓ Yes
Reference
Behavior
Primitives
Reference Types
Assignment
Copies the value
Copies the reference (both point to same object)
Comparison (===)
Compares actual values
Compares references (same object?)
Mutability
Immutable (cannot change)
Mutable (can modify properties)
Storage
Stored directly in variable (stack)
Variable holds reference pointer (heap)
Passing to Functions
Pass by value (copy)
Pass by reference (can mutate original)
Example: Primitive vs Reference behavior
// Primitives: value copied
let a = 5;
let b = a; // Copy value
b = 10;
console.log(a); // 5 (unchanged)
// References: pointer copied
let obj1 = { x: 5 };
let obj2 = obj1; // Copy reference
obj2.x = 10;
console.log(obj1.x); // 10 (both point to same object)
// Comparison
let str1 = "hello";
let str2 = "hello";
console.log(str1 === str2); // true (value comparison)
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false (different references)
console.log(arr1 === arr1); // true (same reference)
// Function parameter behavior
function modifyPrimitive(x) {
x = 100; // Local copy modified
}
let num = 5;
modifyPrimitive(num);
console.log(num); // 5 (original unchanged)
function modifyObject(obj) {
obj.value = 100; // Original object modified
}
let data = { value: 5 };
modifyObject(data);
console.log(data.value); // 100 (original changed)
2.3 Type Conversion and Coercion Rules
Target Type
From String
From Number
From Boolean
From Object
String
Unchanged
"5"
"true"/"false"
toString() or valueOf()
Number
123 or NaN
Unchanged
1 / 0
valueOf() then toString()
Boolean
"" → false, else true
0, NaN → false, else true
Unchanged
Always true
Conversion Method
Syntax
Use Case
Example
Explicit String
String(x), x.toString(), x + ""
Force string conversion
String(123) → "123"
Explicit Number
Number(x), +x, parseInt(), parseFloat()
Force numeric conversion
Number("123") → 123
Explicit Boolean
Boolean(x), !!x
Force boolean conversion
Boolean(0) → false
Implicit Coercion
Operators: +, -, *, /, ==
Automatic conversion by operators
"5" * 2 → 10
Value
to String
to Number
to Boolean
undefined
"undefined"
NaN
false
null
"null"
0
false
true
"true"
1
true
false
"false"
0
false
""
""
0
false
"123"
"123"
123
true
"abc"
"abc"
NaN
true
0, -0
"0"
0
false
NaN
"NaN"
NaN
false
Infinity
"Infinity"
Infinity
true
{}, []
"[object Object]", ""
NaN, 0
true
Example: Type coercion in action
// String coercion (+ with string)
console.log("5" + 3); // "53" (number to string)
console.log("5" + true); // "5true"
// Numeric coercion (other operators)
console.log("5" - 3); // 2 (string to number)
console.log("5" * "2"); // 10
console.log(true + 1); // 2 (true → 1)
console.log(false + 1); // 1 (false → 0)
// Boolean coercion
console.log(Boolean("")); // false
console.log(Boolean(0)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean([])); // true (object)
console.log(Boolean({})); // true (object)
// Explicit conversions
console.log(Number("123")); // 123
console.log(Number("12.5")); // 12.5
console.log(Number("abc")); // NaN
console.log(parseInt("123px")); // 123 (parses until non-digit)
console.log(String(123)); // "123"
console.log(+"42"); // 42 (unary +)
// Falsy values (6 total)
if (!undefined || !null || !0 || !NaN || !"" || !false) {
console.log("All falsy values");
}
Falsy Values (6 total): false, 0, "", null,
undefined, NaN. Everything else is truthy (including [], {},
"0", "false").
2.4 Type Checking (typeof, instanceof, Array.isArray)
Operator/Method
Syntax
Returns
Use Case
Limitations
typeof
typeof value
String type name
Check primitive types
typeof null === "object" (bug); arrays return "object"
instanceof
obj instanceof Constructor
Boolean
Check prototype chain
Doesn't work across frames/windows; primitives always false
Array.isArray()
Array.isArray(value)
Boolean
Reliable array detection
Only for arrays
Object.prototype.toString
Object.prototype.toString.call(value)
"[object Type]"
Most reliable type checking
Verbose syntax
constructor
value.constructor === Type
Boolean
Check constructor
Can be overridden; fails for null/undefined
Value
typeof
instanceof
Best Detection Method
42
"number"
N/A (primitive)
typeof x === "number"
"text"
"string"
N/A (primitive)
typeof x === "string"
true
"boolean"
N/A (primitive)
typeof x === "boolean"
null
"object" ⚠️
false
x === null
undefined
"undefined"
false
x === undefined or typeof x === "undefined"
[]
"object"
true (Array)
Array.isArray(x)
{}
"object"
true (Object)
typeof x === "object" && x !== null
function(){}
"function"
true (Function)
typeof x === "function"
Symbol()
"symbol"
N/A (primitive)
typeof x === "symbol"
42n
"bigint"
N/A (primitive)
typeof x === "bigint"
Example: Type checking techniques
// typeof: basic primitive checking
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 42n); // "bigint"
console.log(typeof null); // "object" ⚠️ (legacy bug)
console.log(typeof []); // "object"
console.log(typeof {}); // "object"
console.log(typeof function(){}); // "function"
// instanceof: prototype chain checking
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Date() instanceof Date); // true
console.log(42 instanceof Number); // false (primitive)
// Array.isArray: reliable array detection
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("array")); // false
// Object.prototype.toString: most reliable
const toString = Object.prototype.toString;
console.log(toString.call([])); // "[object Array]"
console.log(toString.call({})); // "[object Object]"
console.log(toString.call(null)); // "[object Null]"
console.log(toString.call(undefined)); // "[object Undefined]"
console.log(toString.call(new Date())); // "[object Date]"
console.log(toString.call(/regex/)); // "[object RegExp]"
// Utility type checking function
function getType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
console.log(getType(null)); // "null"
console.log(getType([])); // "array"
console.log(getType(42)); // "number"
2.5 Symbol Type and Well-known Symbols
Feature
Description
Syntax
Use Case
Symbol Creation
Creates unique identifier
Symbol("description")
Unique object property keys
Symbol Registry
Global shared symbols
Symbol.for("key")
Cross-realm symbol sharing
Symbol.keyFor()
Get key from registry symbol
Symbol.keyFor(sym)
Retrieve symbol key name
Symbol Properties
Hidden from enumeration
obj[sym] = value
Private-like properties (not truly private)
Well-known Symbol
Purpose
Usage
Symbol.iterator
Defines default iterator
Used by for...of loops, spread operator
Symbol.asyncIterator
Defines async iterator
Used by for await...of loops
Symbol.toStringTag
Customizes toString() output
Changes Object.prototype.toString.call() result
Symbol.toPrimitive
Defines type coercion behavior
Controls conversion to primitive types
Symbol.hasInstance
Customizes instanceof
Overrides instanceof behavior
Symbol.species
Constructor for derived objects
Used in Array/Promise methods
Symbol.match, matchAll
String matching behavior
Used by String.prototype.match()
Symbol.replace, search, split
String manipulation behavior
Used by respective string methods
Example: Symbol usage and well-known symbols
// Basic symbol creation (each is unique)
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false (unique)
// Symbols as object properties
const id = Symbol("id");
const user = {
name: "John",
[id]: 123 // Symbol as computed property
};
console.log(user[id]); // 123
console.log(Object.keys(user)); // ["name"] (symbol hidden)
// Symbol registry (shared across realm)
const globalSym1 = Symbol.for("app.id");
const globalSym2 = Symbol.for("app.id");
console.log(globalSym1 === globalSym2); // true (same symbol)
console.log(Symbol.keyFor(globalSym1)); // "app.id"
// Well-known symbols: Symbol.iterator
const iterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index],
done: index++ >= this.data.length
})
};
}
};
console.log([...iterable]); // [1, 2, 3]
// Symbol.toStringTag
class CustomClass {
get [Symbol.toStringTag]() {
return "CustomClass";
}
}
const obj = new CustomClass();
console.log(Object.prototype.toString.call(obj)); // "[object CustomClass]"
// Symbol.toPrimitive
const obj2 = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 42;
if (hint === "string") return "hello";
return true;
}
};
console.log(+obj2); // 42 (number hint)
console.log(`${obj2}`); // "hello" (string hint)
console.log(obj2 + ""); // "true" (default hint)
2.6 BigInt for Large Integer Operations
Feature
Syntax
Description
Example
BigInt Literal
value + "n"
Append 'n' to integer literal
123n, 0xFFn, 0o77n, 0b1010n
BigInt Constructor
BigInt(value)
Convert number/string to BigInt
BigInt("9007199254740991")
Range
Arbitrary precision
No upper/lower limit (memory-limited)
Can represent integers beyond 253-1
Type
typeof x
Returns "bigint"
typeof 42n === "bigint"
Operation
BigInt Support
Example
Notes
Arithmetic
+, -, *, /, %, **
10n + 20n = 30n
Both operands must be BigInt
Comparison
<, >, <=, >=, ==, !=
10n < 20n, 10n == 10
== coerces; === is strict
Bitwise
&, |, ^, ~, <<, >>
5n & 3n = 1n
>>> not supported (unsigned)
Division
/ (integer division)
7n / 2n = 3n
Truncates decimal (rounds toward zero)
Unary Minus
-x
-42n = -42n
Negation supported
Unary Plus
❌ Not supported
+42n ❌ TypeError
Use Number() to convert
Limitation
Description
Workaround
No Mixed Operations
Cannot mix BigInt with Number
Explicitly convert: 10n + BigInt(5) or Number(10n) + 5
No Decimal Values
BigInt only for integers
Use Number for decimal/floating-point
Math Object
Math.* methods don't support BigInt
Implement custom logic or convert to Number
JSON Serialization
JSON.stringify() throws TypeError
Custom replacer: JSON.stringify(obj, (k,v) => typeof v === 'bigint' ? v.toString() : v)
Implicit Coercion
No automatic type coercion
Always explicit conversion required
Example: BigInt operations and limitations
// Creating BigInts
const big1 = 9007199254740991n;
const big2 = BigInt("9007199254740991");
const big3 = BigInt(Number.MAX_SAFE_INTEGER);
// Arithmetic operations
console.log(100n + 50n); // 150n
console.log(100n - 50n); // 50n
console.log(100n * 2n); // 200n
console.log(100n / 3n); // 33n (integer division, truncates)
console.log(100n % 3n); // 1n
console.log(2n ** 100n); // Very large number (2^100)
// Comparison
console.log(10n === 10); // false (strict equality)
console.log(10n == 10); // true (loose equality with coercion)
console.log(10n < 20n); // true
console.log(5n > 3); // true (coercion in comparison)
// Limitations
// console.log(10n + 5); // ❌ TypeError: Cannot mix BigInt and Number
console.log(10n + BigInt(5)); // ✓ 15n (explicit conversion)
console.log(Number(10n) + 5); // ✓ 15 (convert to Number)
// console.log(+10n); // ❌ TypeError: Cannot convert BigInt
console.log(Number(10n)); // ✓ 10 (explicit conversion)
// Bitwise operations
console.log(5n & 3n); // 1n (binary AND)
console.log(5n | 3n); // 7n (binary OR)
console.log(5n ^ 3n); // 6n (binary XOR)
console.log(5n << 2n); // 20n (left shift)
// Use case: Precise large integer calculations
const largeNumber = 9007199254740992n; // Beyond Number.MAX_SAFE_INTEGER
console.log(largeNumber + 1n); // 9007199254740993n (exact)
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992 (precision lost)
Warning: BigInt cannot be mixed with regular numbers in arithmetic operations. Always
explicitly convert using BigInt() or Number().
Section 2 Summary
- JavaScript has 7 primitive types: string, number, boolean, null, undefined,
symbol, bigint
- Primitives are immutable and passed by value; reference types are mutable and passed by reference
- Type coercion happens implicitly with operators; 6 falsy values exist:
false, 0, "", null, undefined, NaN
- Use
typeof for primitives, Array.isArray() for arrays, and
instanceof for prototype checking
- Symbols provide unique identifiers for object properties; well-known
symbols customize language behavior
- BigInt enables arbitrary-precision integer math beyond
Number.MAX_SAFE_INTEGER (253-1)
3. Variables, Scope, and Closure
3.1 Variable Hoisting and Temporal Dead Zone
Declaration
Hoisting Behavior
Initialization
TDZ
Access Before Declaration
var
Hoisted to function/global scope
Initialized to undefined
❌ No TDZ
Returns undefined
let
Hoisted to block scope
Not initialized
✓ Has TDZ
ReferenceError
const
Hoisted to block scope
Not initialized
✓ Has TDZ
ReferenceError
function (declaration)
Fully hoisted with definition
Fully initialized
❌ No TDZ
Callable before declaration
function (expression)
Variable hoisted only
Depends on var/let/const
Depends on var/let/const
Not callable (undefined or TDZ)
class
Hoisted to block scope
Not initialized
✓ Has TDZ
ReferenceError
Concept
Description
Key Points
Hoisting
JavaScript moves declarations to top of their scope during compilation
Only declarations hoisted, not initializations
Temporal Dead Zone (TDZ)
Time between entering scope and variable initialization where access causes error
Exists for let, const, and class
TDZ Start
Beginning of enclosing block scope
Not visible in code; conceptual time period
TDZ End
Point where variable is declared and initialized
Variable becomes usable after this point
Example: Hoisting and TDZ behavior
// var hoisting: declaration hoisted, initialized to undefined
console.log(x); // undefined (no error)
var x = 5;
console.log(x); // 5
// let/const TDZ: declaration hoisted but not initialized
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
console.log(y); // 10
// Function declaration: fully hoisted
greet(); // "Hello" (works before declaration)
function greet() {
console.log("Hello");
}
// Function expression: only variable hoisted
// sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi");
};
sayHi(); // "Hi"
// TDZ example with block scope
{
// TDZ starts here for 'temp'
// console.log(temp); // ReferenceError
// const result = doSomething(temp); // ReferenceError
let temp = 5; // TDZ ends here
console.log(temp); // 5 (now accessible)
}
// Class TDZ
// const p = new Person(); // ReferenceError
class Person {
constructor(name) { this.name = name; }
}
const p = new Person("John"); // Works after declaration
Note: TDZ prevents using variables before initialization, catching potential bugs. Always
declare variables at the top of their scope for clarity.
3.2 Scope Types (Global, Function, Block, Module)
Scope Type
Created By
Variables
Lifetime
Access
Global Scope
Outside all functions/blocks
var, let, const, functions
Entire program execution
Accessible everywhere
Function Scope
Function declarations/expressions
var, parameters, inner functions
Function execution
Within function and nested functions
Block Scope
{ } braces, loops, if/switch
let, const, class
Block execution
Within block only
Module Scope
ES6 modules (.js files with import/export)
All declarations in module file
Module lifetime
Isolated; explicit exports required
Catch Block Scope
catch(error) { }
Error parameter, let, const
Catch block execution
Within catch block only
Declaration
Global Scope
Function Scope
Block Scope
var
✓ Function or global
✓ Function-scoped
❌ Ignores blocks
let
✓ Global (no window property)
✓ Function-scoped
✓ Block-scoped
const
✓ Global (no window property)
✓ Function-scoped
✓ Block-scoped
function
✓ Global
✓ Function-scoped
❌ Block-scoped in strict mode only
Example: Different scope types
// Global scope
var globalVar = 'global var';
let globalLet = 'global let';
const globalConst = 'global const';
function demoScopes() {
// Function scope
var functionVar = 'function scoped';
let functionLet = 'function scoped';
console.log(globalVar); // Accessible
if (true) {
// Block scope
var blockVar = 'not block scoped (var)';
let blockLet = 'block scoped';
const blockConst = 'block scoped';
console.log(functionVar); // Accessible from outer function
console.log(blockLet); // Accessible within block
}
console.log(blockVar); // Accessible (var ignores blocks)
// console.log(blockLet); // ReferenceError (block-scoped)
// Loop scope
for (let i = 0; i < 3; i++) {
// 'i' is block-scoped to this loop
setTimeout(() => console.log(i), 100); // Prints 0, 1, 2
}
for (var j = 0; j < 3; j++) {
// 'j' is function-scoped (shared)
setTimeout(() => console.log(j), 100); // Prints 3, 3, 3
}
}
// Module scope (in ES6 modules)
// Variables not automatically global
// export const moduleVar = 'exported';
// const privateVar = 'not exported';
// Catch block scope
try {
throw new Error('test');
} catch (error) {
// 'error' is scoped to catch block
let catchVar = 'catch scoped';
console.log(error.message);
}
// console.log(error); // ReferenceError
// console.log(catchVar); // ReferenceError
Warning: Global variables create properties on the global object (window in
browsers) with var, but not with let/const. Avoid global variables to
prevent naming conflicts.
3.3 Lexical Scope and Scope Chain
Concept
Description
Behavior
Lexical Scope
Scope determined by code location (where functions are written)
Inner functions access outer function variables
Scope Chain
Hierarchy of nested scopes searched for variable resolution
Searches from inner → outer until found or reaches global
Static Scoping
Another name for lexical scoping (opposite of dynamic scoping)
Scope determined at write-time, not runtime
Outer Environment Reference
Link from execution context to parent scope
Created when function is defined, not when called
Variable Lookup Process
Step
Action
Step 1
Current Scope
Check if variable exists in current local scope
Step 2
Parent Scope
If not found, move to enclosing (parent) scope
Step 3
Repeat
Continue up the scope chain through ancestors
Step 4
Global Scope
Check global scope as last resort
Step 5
Not Found
ReferenceError if not found anywhere in chain
Example: Lexical scope and scope chain
// Scope chain demonstration
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
const innerVar = 'inner';
// Scope chain: inner → middle → outer → global
console.log(innerVar); // Found in inner scope
console.log(middleVar); // Found in middle scope (parent)
console.log(outerVar); // Found in outer scope (grandparent)
console.log(globalVar); // Found in global scope
// Variable lookup order:
// 1. Check inner scope
// 2. Check middle scope
// 3. Check outer scope
// 4. Check global scope
// 5. ReferenceError if not found
}
inner();
// console.log(innerVar); // ReferenceError (not in scope chain)
}
middle();
}
outer();
// Lexical scope is determined by code structure
function makeCounter() {
let count = 0; // In outer function scope
return function() {
// Lexically scoped to access 'count' from parent
return ++count;
};
}
const counter1 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
const counter2 = makeCounter();
console.log(counter2()); // 1 (separate scope chain)
// Scope determined at definition, not invocation
let x = 'global x';
function showX() {
console.log(x); // Lexically bound to global 'x'
}
function demo() {
let x = 'local x';
showX(); // Prints "global x" (not "local x")
// Scope determined where showX was defined
}
demo();
Note: Lexical scope is determined by where the function is written, not where it's called. This enables closures
and predictable variable access.
3.4 Variable Shadowing and Name Resolution
Concept
Description
Behavior
Recommendation
Variable Shadowing
Inner variable hides outer variable with same name
Inner scope variable takes precedence
Avoid shadowing; use different names
Name Resolution
Process of finding which variable identifier refers to
Searches scope chain from inner to outer
Use descriptive, unique names
Parameter Shadowing
Function parameter shadows outer variable
Parameter accessible, outer variable hidden
Common pattern; usually intentional
Block Shadowing
Block-scoped variable shadows outer scope
Only within block; outer restored after
Use for temporary local variables
Shadowing Type
Outer
Inner
Allowed?
Behavior
var shadows var
var
var
✓ Yes
Redeclaration in same scope or shadowing in nested
let/const shadows var
var
let/const
✓ Yes
Inner hides outer; outer unchanged
var shadows let/const
let/const
var
Depends
Error if same function scope; OK if nested function
let/const shadows let/const
let/const
let/const
✓ In different block
✗ SyntaxError in same block
Parameter shadowing
Any
Parameter
✓ Yes
Parameter has priority in function
Example: Variable shadowing scenarios
// Basic shadowing
let x = 'outer';
function demo() {
let x = 'inner'; // Shadows outer 'x'
console.log(x); // "inner"
}
demo();
console.log(x); // "outer" (outer unchanged)
// Parameter shadowing
let value = 100;
function processValue(value) { // Parameter shadows outer 'value'
console.log(value); // Uses parameter, not outer variable
value = 200; // Modifies parameter only
}
processValue(50); // Prints 50
console.log(value); // 100 (outer unchanged)
// Block shadowing
let count = 10;
if (true) {
let count = 20; // Shadows outer 'count' in block
console.log(count); // 20
}
console.log(count); // 10 (outer restored after block)
// Nested function shadowing
function outer() {
let name = 'outer';
function inner() {
let name = 'inner'; // Shadows parent function's 'name'
console.log(name); // "inner"
}
inner();
console.log(name); // "outer"
}
outer();
// Loop variable shadowing
for (let i = 0; i < 3; i++) {
console.log(i); // Loop 'i'
for (let i = 0; i < 2; i++) {
console.log(' ' + i); // Inner loop 'i' shadows outer
}
}
// Problematic shadowing to avoid
function calculateTotal() {
let total = 0;
if (true) {
let total = 100; // Shadowing can be confusing
// Which 'total' is which?
}
return total; // Returns 0, not 100
}
Warning: While shadowing is allowed, excessive use can make code confusing. Use distinct
variable names for clarity unless shadowing is intentional (e.g., function parameters).
3.5 Closure Patterns and Memory Management
Concept
Definition
Key Characteristics
Closure
Function bundled with its lexical environment (surrounding state)
Retains access to outer scope variables even after outer function returns
Closure Creation
Created every time a function is created
Inner function "closes over" variables from outer scope
Closure Use Cases
Data privacy, factory functions, callbacks, event handlers
Maintains state between function calls
Memory Retention
Closed-over variables remain in memory
Can cause memory leaks if not managed properly
Closure Pattern
Use Case
Example Structure
Private Variables
Encapsulation, data hiding
Function returns methods that access private variables
Factory Functions
Create objects with private state
Function returns object with methods (closures)
Module Pattern
Namespace management, public/private API
IIFE returns object with public methods
Callbacks/Event Handlers
Preserve context for async operations
Function captures variables for later execution
Currying/Partial Application
Function transformation, configuration
Function returns function with captured arguments
Memoization
Performance optimization, caching
Closure maintains cache of computed values
Example: Common closure patterns
// 1. Private variables pattern
function createCounter() {
let count = 0; // Private variable
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined (private)
// 2. Factory function pattern
function createUser(name) {
let _name = name; // Private
let _sessions = 0;
return {
getName: () => _name,
login: () => ++_sessions,
getSessions: () => _sessions
};
}
const user1 = createUser("Alice");
const user2 = createUser("Bob");
user1.login();
console.log(user1.getSessions()); // 1
console.log(user2.getSessions()); // 0
// 3. Module pattern with IIFE
const calculator = (function() {
let history = []; // Private state
return {
add: (a, b) => {
const result = a + b;
history.push(`${a} + ${b} = ${result}`);
return result;
},
getHistory: () => [...history] // Return copy
};
})();
calculator.add(5, 3);
console.log(calculator.getHistory()); // ["5 + 3 = 8"]
// 4. Callback closure preserving context
function fetchDataWithRetry(url, retries) {
let attempts = 0; // Captured by callback
function attemptFetch() {
fetch(url)
.catch(error => {
if (++attempts < retries) {
console.log(`Retry ${attempts}`);
attemptFetch(); // Closure accesses 'attempts'
}
});
}
attemptFetch();
}
// 5. Currying with closures
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const multiplyBy2 = multiply(2);
const multiplyBy2And3 = multiplyBy2(3);
console.log(multiplyBy2And3(4)); // 24
// 6. Memoization pattern
function memoize(fn) {
const cache = {}; // Closure maintains cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const factorial = memoize(n => n <= 1 ? 1 : n * factorial(n - 1));
console.log(factorial(5)); // Computed: 120
console.log(factorial(5)); // Cached: 120
Memory Consideration
Issue
Solution
Retained Variables
Closed-over variables can't be garbage collected
Clear references when done (obj = null)
Event Listeners
Closure in listener prevents GC of entire scope
Remove listeners: removeEventListener
Timers/Intervals
Callback closures keep references alive
Clear timers: clearTimeout/clearInterval
Large Data Structures
Closure inadvertently captures large objects
Extract only needed data before creating closure
Note: Closures are powerful but retain references to their outer scope. Be mindful of memory
usage, especially with long-lived closures or large captured variables.
3.6 Module Scope and Variable Isolation
Module Feature
Description
Benefit
Module Scope
Each ES6 module has its own scope (file-level)
Variables don't pollute global scope by default
Variable Isolation
Module variables are private unless explicitly exported
Prevents naming conflicts between modules
Strict Mode
Modules automatically run in strict mode
No need for 'use strict'; directive
Top-level this
this is undefined at module top level
Not bound to global object (safer)
Static Imports
Import declarations hoisted and executed first
Enables static analysis and tree-shaking
Module Pattern
Syntax
Use Case
Named Export
export const x = 1;
Export multiple items from module
Default Export
export default function() {}
Main export from module (one per module)
Named Import
import { x, y } from './mod';
Import specific exports
Default Import
import mod from './mod';
Import default export
Namespace Import
import * as mod from './mod';
Import all exports as object
Re-export
export { x } from './mod';
Export from another module (barrel pattern)
Dynamic Import
import('./mod').then(...)
Load modules conditionally/lazily
Example: Module scope and isolation
// ===== math.js (Module A) =====
// Private variables (not exported)
const PI = 3.14159;
let calculationCount = 0;
// Private helper function
function log(operation) {
calculationCount++;
console.log(`Operation: ${operation}, Count: ${calculationCount}`);
}
// Named exports (public API)
export function add(a, b) {
log('add');
return a + b;
}
export function multiply(a, b) {
log('multiply');
return a * b;
}
export const TAU = 2 * PI; // Export constant
// Default export
export default function calculate(expr) {
return eval(expr); // Example only
}
// ===== user.js (Module B) =====
// Module variables are isolated
const userCount = 0; // Won't conflict with other modules
class User {
constructor(name) {
this.name = name;
}
}
export { User, userCount };
// ===== app.js (Main module) =====
// Import from math.js
import calculate, { add, multiply, TAU } from './math.js';
// Import from user.js
import { User, userCount } from './user.js';
// Module-scoped variables (private to this module)
const appName = 'MyApp';
let config = { debug: true };
console.log(add(5, 3)); // 8
console.log(multiply(2, 4)); // 8
console.log(TAU); // 6.28318
// Cannot access private variables from math.js
// console.log(PI); // ReferenceError
// console.log(calculationCount); // ReferenceError
const user = new User('Alice');
// Top-level 'this' is undefined in modules
console.log(this); // undefined (not window)
// ===== Dynamic imports =====
async function loadModule() {
if (config.debug) {
const debugModule = await import('./debug.js');
debugModule.log('Debug mode enabled');
}
}
// ===== Barrel export pattern (index.js) =====
// Re-export from multiple modules
export { add, multiply } from './math.js';
export { User } from './user.js';
export { default as calculate } from './math.js';
Note: ES6 modules provide true encapsulation with file-level scope. Variables are private by
default, eliminating need for IIFE patterns and reducing global scope pollution.
Section 3 Summary
- Hoisting moves declarations to top of scope;
let/const/class have TDZ (ReferenceError before initialization)
- 4 scope types: Global (everywhere), Function (var), Block (let/const),
Module (file-level isolation)
- Lexical scope determined by code structure; scope chain searches inner →
outer → global for variables
- Variable shadowing occurs when inner scope variable hides outer one; use
distinct names for clarity
- Closures retain access to outer scope variables; enable private state but
require memory management
- ES6 modules provide true isolation with file-level scope; variables private
unless exported
4. Operators and Expressions Reference
4.1 Arithmetic Operators and Math Operations
Operator
Name
Description
Example
Result
+
Addition / Unary Plus
Adds numbers or concatenates strings; converts to number
5 + 3, +"42"
8, 42
-
Subtraction / Unary Minus
Subtracts numbers; negates value
5 - 3, -5
2, -5
*
Multiplication
Multiplies numbers
5 * 3
15
/
Division
Divides numbers (floating-point result)
10 / 3
3.3333...
%
Remainder / Modulo
Returns remainder of division
10 % 3
1
**
Exponentiation ES2016
Raises to power
2 ** 3
8
++
Increment
Increases by 1 (prefix or postfix)
++x, x++
Pre: return new; Post: return old
--
Decrement
Decreases by 1 (prefix or postfix)
--x, x--
Pre: return new; Post: return old
Operation Type
Behavior
Special Cases
Number + Number
Numeric addition
5 + 3 = 8
String + Any
String concatenation
"5" + 3 = "53", "a" + true = "atrue"
Division by Zero
Returns Infinity
5 / 0 = Infinity, -5 / 0 = -Infinity
0 / 0
Returns NaN
Indeterminate result
NaN in Operations
Propagates NaN
NaN + 5 = NaN, NaN * 2 = NaN
Infinity Operations
Mathematical infinity rules
Infinity + 1 = Infinity, Infinity * -1 = -Infinity
Example: Arithmetic operators and edge cases
// Basic arithmetic
console.log(10 + 5); // 15
console.log(10 - 5); // 5
console.log(10 * 5); // 50
console.log(10 / 5); // 2
console.log(10 % 3); // 1 (remainder)
console.log(2 ** 3); // 8 (exponentiation)
// Increment/Decrement
let x = 5;
console.log(x++); // 5 (returns old value, then increments)
console.log(x); // 6
console.log(++x); // 7 (increments, then returns new value)
console.log(x--); // 7 (returns old value, then decrements)
console.log(--x); // 5 (decrements, then returns new value)
// String concatenation with +
console.log("Hello" + " " + "World"); // "Hello World"
console.log("5" + 3); // "53" (string concatenation)
console.log(5 + "3"); // "53"
console.log("5" + 3 + 2); // "532"
console.log(5 + 3 + "2"); // "82" (left to right: 5+3=8, then 8+"2"="82")
// Type coercion with other operators
console.log("10" - 5); // 5 (string to number)
console.log("10" * "2"); // 20
console.log("10" / "2"); // 5
console.log("10" % 3); // 1
// Unary operators
console.log(+"42"); // 42 (string to number)
console.log(+"abc"); // NaN
console.log(-"5"); // -5
// Special values
console.log(5 / 0); // Infinity
console.log(-5 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(NaN + 10); // NaN (NaN propagates)
console.log(Infinity - 1); // Infinity
4.2 Assignment Operators and Compound Assignment
Operator
Name
Equivalent
Example
Result
=
Simple Assignment
N/A
x = 5
x is 5
+=
Addition Assignment
x = x + y
x += 3
Adds and assigns
-=
Subtraction Assignment
x = x - y
x -= 3
Subtracts and assigns
*=
Multiplication Assignment
x = x * y
x *= 3
Multiplies and assigns
/=
Division Assignment
x = x / y
x /= 3
Divides and assigns
%=
Remainder Assignment
x = x % y
x %= 3
Remainder and assigns
**=
Exponentiation Assignment
x = x ** y
x **= 3
Exponentiates and assigns
<<=
Left Shift Assignment
x = x << y
x <<= 2
Left shift and assigns
>>=
Right Shift Assignment
x = x >> y
x >>= 2
Right shift and assigns
>>>=
Unsigned Right Shift Assignment
x = x >>> y
x >>>= 2
Unsigned shift and assigns
&=
Bitwise AND Assignment
x = x & y
x &= 3
AND and assigns
|=
Bitwise OR Assignment
x = x | y
x |= 3
OR and assigns
^=
Bitwise XOR Assignment
x = x ^ y
x |= 3
XOR and assigns
Feature
Description
Example
Destructuring Assignment
Unpack values from arrays/objects
[a, b] = [1, 2], {x, y} = obj
Multiple Assignment
Chain assignments (right-to-left)
a = b = c = 5 (all get 5)
Return Value
Assignment returns assigned value
y = (x = 5) (both are 5)
Compound Benefits
Shorter, clearer, evaluates left side once
arr[i++] += 5 increments i once
Example: Assignment operators
// Simple assignment
let x = 10;
// Compound assignments
x += 5; // x = x + 5; x is now 15
x -= 3; // x = x - 3; x is now 12
x *= 2; // x = x * 2; x is now 24
x /= 4; // x = x / 4; x is now 6
x %= 5; // x = x % 5; x is now 1
x **= 3; // x = x ** 3; x is now 1
// String concatenation with +=
let str = "Hello";
str += " World"; // str is "Hello World"
// Multiple assignment (right-to-left)
let a, b, c;
a = b = c = 5; // All are 5
console.log(a, b, c); // 5 5 5
// Assignment returns value
let y = (x = 10); // x is 10, y is 10
console.log(x, y); // 10 10
// Destructuring assignment
let [p, q] = [1, 2];
console.log(p, q); // 1 2
let {name, age} = {name: "John", age: 30};
console.log(name, age); // "John" 30
// Compound assignment with side effects
let arr = [10, 20, 30];
let i = 0;
arr[i++] += 5; // arr[0] becomes 15, then i increments
console.log(arr[0], i); // 15 1
// Swapping with destructuring
let m = 1, n = 2;
[m, n] = [n, m]; // Swap
console.log(m, n); // 2 1
4.3 Comparison Operators and Equality Rules
Operator
Name
Description
Type Coercion
Example
==
Equality (loose)
Checks value equality with type coercion
✓ Yes
5 == "5" → true
===
Strict Equality
Checks value and type equality
✗ No
5 === "5" → false
!=
Inequality (loose)
Checks value inequality with coercion
✓ Yes
5 != "6" → true
!==
Strict Inequality
Checks value or type inequality
✗ No
5 !== "5" → true
<
Less Than
Checks if left < right
✓ Yes
5 < 10 → true
>
Greater Than
Checks if left > right
✓ Yes
10 > 5 → true
<=
Less Than or Equal
Checks if left ≤ right
✓ Yes
5 <= 5 → true
>=
Greater Than or Equal
Checks if left ≥ right
✓ Yes
10 >= 10 → true
Comparison
== (loose)
=== (strict)
Reason
5 vs "5"
true
false
String coerced to number
0 vs false
true
false
Boolean coerced to number
null vs undefined
true
false
Special case: only equal to each other
"" vs 0
true
false
Empty string coerced to 0
NaN vs NaN
false
false
NaN not equal to itself (use isNaN())
[] vs []
false
false
Different object references
0 vs -0
true
true
Considered equal (use Object.is() to distinguish)
Example: Equality and comparison operators
// Strict equality (===) - recommended
console.log(5 === 5); // true
console.log(5 === "5"); // false (different types)
console.log(null === null); // true
console.log(undefined === undefined); // true
// Loose equality (==) - with type coercion
console.log(5 == "5"); // true (string coerced to number)
console.log(0 == false); // true (boolean to number)
console.log("" == 0); // true (string to number)
console.log(null == undefined); // true (special case)
// Inequality
console.log(5 !== "5"); // true (strict)
console.log(5 != "6"); // true (loose)
// Relational operators
console.log(5 < 10); // true
console.log(10 > 5); // true
console.log(5 <= 5); // true
console.log(10 >= 10); // true
// String comparison (lexicographic)
console.log("apple" < "banana"); // true (a < b)
console.log("10" < "9"); // true (string comparison: "1" < "9")
console.log(10 < "9"); // false (number comparison: 10 > 9)
// Special cases
console.log(NaN === NaN); // false (use Number.isNaN())
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true (correct way)
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false (distinguishes +0 and -0)
// Object comparison (reference)
const obj1 = {a: 1};
const obj2 = {a: 1};
const obj3 = obj1;
console.log(obj1 === obj2); // false (different references)
console.log(obj1 === obj3); // true (same reference)
// Array comparison
console.log([1,2] == [1,2]); // false (different references)
console.log([1,2] === [1,2]); // false
// Tricky comparisons to avoid
console.log([] == ![]); // true (![] is false, [] coerced to "")
console.log([] == false); // true
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("0" == 0); // true
console.log("0" == false); // true
Warning: Always use === and !== (strict equality) to avoid unexpected
type coercion bugs. Use == only when you specifically need type coercion.
4.4 Logical Operators and Short-circuit Evaluation
Operator
Name
Returns
Short-circuits
Use Case
&&
Logical AND
First falsy value or last value
✓ If left is falsy
Both conditions must be true
||
Logical OR
First truthy value or last value
✓ If left is truthy
At least one condition must be true
!
Logical NOT
Boolean (inverted)
✗ No
Inverts truthiness
!!
Double NOT
Boolean conversion
✗ No
Convert to boolean explicitly
Expression
Result
Explanation
true && true
true
Both truthy → returns last value
true && false
false
Right is falsy → returns false
false && true
false
Short-circuits at false (left)
5 && "hello"
"hello"
Both truthy → returns last
0 && "hello"
0
First falsy → returns 0
true || false
true
Short-circuits at first truthy
false || true
true
Returns first truthy value
false || false
false
Both falsy → returns last
5 || "hello"
5
First truthy → short-circuits
0 || "hello"
"hello"
First falsy, returns second
!true
false
Inverts boolean
!0
true
0 is falsy, inverted to true
!!"hello"
true
Convert to boolean: truthy
Example: Logical operators and short-circuit evaluation
// Logical AND (&&) - returns first falsy or last value
console.log(true && true); // true
console.log(true && false); // false
console.log(5 && 10); // 10 (both truthy, returns last)
console.log(0 && 10); // 0 (first falsy)
console.log(null && "hello"); // null (first falsy)
// Logical OR (||) - returns first truthy or last value
console.log(false || true); // true
console.log(false || false); // false (last value)
console.log(5 || 10); // 5 (first truthy)
console.log(0 || 10); // 10 (first falsy, returns second)
console.log(null || "default"); // "default" (first falsy)
// Logical NOT (!)
console.log(!true); // false
console.log(!false); // true
console.log(!0); // true (0 is falsy)
console.log(!""); // true ("" is falsy)
console.log(!"hello"); // false ("hello" is truthy)
// Double NOT (!!) - convert to boolean
console.log(!!"hello"); // true
console.log(!!0); // false
console.log(!!""); // false
console.log(!!{}); // true (objects are truthy)
// Short-circuit evaluation - practical uses
// 1. Default values (before ?? was added)
let username = input || "Guest"; // If input is falsy, use "Guest"
// 2. Conditional execution
isLoggedIn && showDashboard(); // Execute if truthy
hasError || showSuccessMessage(); // Execute if falsy
// 3. Guard clauses
function processUser(user) {
user && user.profile && console.log(user.profile.name);
// Only accesses nested property if all are truthy
}
// 4. Avoiding function calls
let result = expensiveCheck() || cachedValue;
// If expensiveCheck() is truthy, cachedValue never evaluated
// Short-circuit prevents errors
let obj = null;
let value = obj && obj.property; // undefined (doesn't throw)
// Without short-circuit: obj.property would throw error
// Combining operators
let access = isAdmin || (isPremium && hasPermission);
console.log(true || (false && error())); // true, error() not called
// Falsy values: false, 0, "", null, undefined, NaN
console.log(false || 0 || "" || null || undefined || NaN || "found");
// "found" (first truthy)
Note: Logical operators return the actual value (not just boolean). Short-circuit evaluation
stops at first definitive result, useful for default values and conditional execution.
4.5 Bitwise Operators and Binary Operations
Operator
Name
Description
Example
Binary
Result
&
AND
1 if both bits are 1
5 & 3
0101 & 0011
1 (0001)
|
OR
1 if at least one bit is 1
5 | 3
0101 | 0011
7 (0111)
^
XOR
1 if bits are different
5 ^ 3
0101 ^ 0011
6 (0110)
~
NOT
Inverts all bits
~5
~0101
-6 (two's complement)
<<
Left Shift
Shifts bits left, fills 0s
5 << 1
0101 → 1010
10
>>
Sign-propagating Right Shift
Shifts bits right, preserves sign
5 >> 1
0101 → 0010
2
>>>
Zero-fill Right Shift
Shifts bits right, fills 0s
-5 >>> 1
Fills with 0s
Large positive number
Use Case
Operation
Example
Purpose
Check if even
n & 1
5 & 1 = 1 (odd), 4 & 1 = 0 (even)
Fast even/odd check
Multiply by 2n
n << x
5 << 2 = 20 (5 × 4)
Fast multiplication
Divide by 2n
n >> x
20 >> 2 = 5 (20 ÷ 4)
Fast division
Toggle bit
n ^ (1 << i)
Toggles i-th bit
Flags/permissions
Set bit
n | (1 << i)
Sets i-th bit to 1
Enable flag
Clear bit
n & ~(1 << i)
Sets i-th bit to 0
Disable flag
Check bit
(n >> i) & 1
Gets value of i-th bit
Test flag
Swap variables
a ^= b; b ^= a; a ^= b;
Swap without temp variable
XOR swap trick
Example: Bitwise operations
// Basic bitwise operations
console.log(5 & 3); // 1 (0101 & 0011 = 0001)
console.log(5 | 3); // 7 (0101 | 0011 = 0111)
console.log(5 ^ 3); // 6 (0101 ^ 0011 = 0110)
console.log(~5); // -6 (inverts bits, two's complement)
// Bit shifts
console.log(5 << 1); // 10 (0101 → 1010) multiply by 2
console.log(5 << 2); // 20 (0101 → 10100) multiply by 4
console.log(10 >> 1); // 5 (1010 → 0101) divide by 2
console.log(20 >> 2); // 5 (10100 → 0101) divide by 4
// Practical use cases
// 1. Check if number is even or odd
function isEven(n) {
return (n & 1) === 0;
}
console.log(isEven(4)); // true
console.log(isEven(5)); // false
// 2. Flags and permissions
const READ = 1; // 001
const WRITE = 2; // 010
const EXECUTE = 4; // 100
let permissions = 0;
permissions |= READ; // Add READ permission
permissions |= WRITE; // Add WRITE permission
console.log(permissions & READ); // Check if READ (non-zero = true)
console.log(permissions & EXECUTE); // Check if EXECUTE (0 = false)
permissions &= ~WRITE; // Remove WRITE permission
console.log(permissions); // 1 (only READ)
// 3. Fast integer conversion
console.log(~~3.14); // 3 (double NOT truncates decimals)
console.log(5.7 | 0); // 5 (OR with 0 truncates)
console.log(5.7 >> 0); // 5 (right shift by 0 truncates)
// 4. XOR swap (without temp variable)
let a = 5, b = 10;
a ^= b; // a = 5 ^ 10
b ^= a; // b = 10 ^ (5 ^ 10) = 5
a ^= b; // a = (5 ^ 10) ^ 5 = 10
console.log(a, b); // 10 5
// 5. Toggle boolean
let flag = true;
flag ^= 1; // false (1 ^ 1 = 0)
flag ^= 1; // true (0 ^ 1 = 1)
// 6. Set specific bit
let n = 0b0000; // Binary literal
n |= (1 << 2); // Set bit 2: 0b0100
console.log(n.toString(2)); // "100"
// 7. Clear specific bit
n &= ~(1 << 2); // Clear bit 2: 0b0000
console.log(n.toString(2)); // "0"
Note: Bitwise operators work on 32-bit integers. Common uses include flags, permissions, fast
math operations, and low-level data manipulation.
4.6 Optional Chaining (?.) and Nullish Coalescing (??)
Operator
Syntax
Returns
Use Case
?. ES2020
obj?.prop
undefined if obj is null/undefined, else obj.prop
Safe property access
?.[]
obj?.[expr]
undefined if obj is null/undefined
Safe dynamic property access
?.()
func?.(args)
undefined if func is null/undefined
Safe function call
?? ES2020
a ?? b
a if not null/undefined, else b
Default values for null/undefined only
Feature
|| (OR)
?? (Nullish Coalescing)
Falsy values treated as missing
Yes (false, 0, "", null, undefined, NaN)
No (only null/undefined)
0 || 100
100
0
"" || "default"
"default"
""
false || true
true
false
null ?? 100
100
100
undefined ?? 100
100
100
Use case
When any falsy should use default
When only null/undefined should use default
Example: Optional chaining and nullish coalescing
// Optional Chaining (?.)
// Without optional chaining (verbose and error-prone)
let user = null;
// let name = user.profile.name; // TypeError: Cannot read property 'profile' of null
let name = user && user.profile && user.profile.name; // undefined
// With optional chaining (concise and safe)
name = user?.profile?.name; // undefined (no error)
const user2 = {
profile: {
name: "Alice",
address: {
city: "NYC"
}
}
};
console.log(user2?.profile?.name); // "Alice"
console.log(user2?.profile?.address?.city); // "NYC"
console.log(user2?.profile?.phone); // undefined (no error)
console.log(user2?.account?.balance); // undefined
// Optional chaining with arrays
const arr = null;
console.log(arr?.[0]); // undefined (safe)
console.log(user2?.tags?.[0]); // undefined
// Optional chaining with function calls
const obj = {
method: function() { return "called"; }
};
console.log(obj.method?.()); // "called"
console.log(obj.nonExistent?.()); // undefined (no error)
console.log(user?.getProfile?.()); // undefined
// Nullish Coalescing (??)
// Problem with || operator
let count = 0;
let value1 = count || 10; // 10 (0 is falsy, uses default)
let value2 = count ?? 10; // 0 (0 is not null/undefined)
let text = "";
let msg1 = text || "default"; // "default" ("" is falsy)
let msg2 = text ?? "default"; // "" (empty string is valid)
// Use ?? for actual null/undefined checks
let config = {
timeout: 0, // 0 is valid
enabled: false, // false is valid
name: "" // empty string is valid
};
let timeout = config.timeout ?? 3000; // 0 (not null/undefined)
let enabled = config.enabled ?? true; // false (not null/undefined)
let name = config.name ?? "Unnamed"; // "" (not null/undefined)
let missing = config.missing ?? "N/A"; // "N/A" (undefined)
// Combining ?. and ??
const data = {
user: null,
settings: { theme: "dark" }
};
let theme = data?.settings?.theme ?? "light"; // "dark"
let userName = data?.user?.name ?? "Guest"; // "Guest"
// Real-world example: API response
function getUserCity(response) {
return response?.data?.user?.address?.city ?? "Unknown";
}
console.log(getUserCity(null)); // "Unknown"
console.log(getUserCity({ data: null })); // "Unknown"
console.log(getUserCity({
data: { user: { address: { city: "NYC" } } }
})); // "NYC"
// Short-circuit evaluation
let expensiveFn = () => { console.log("Called"); return 42; };
let result = null?.property?.method?.(); // undefined (expensiveFn not called)
console.log(result); // undefined
Warning: Don't overuse ?. as it can hide bugs. Use it for genuinely optional
properties, not to mask errors in required data.
4.7 Logical Assignment Operators (||=, &&=, ??=)
Operator
Name
Equivalent
Assigns When
||= ES2021
Logical OR Assignment
x || (x = y)
x is falsy
&&= ES2021
Logical AND Assignment
x && (x = y)
x is truthy
??= ES2021
Nullish Assignment
x ?? (x = y)
x is null or undefined
Operator
Behavior
Use Case
Example
||=
Assigns if current value is falsy
Set default for any falsy value
x ||= 10 (assigns if x is falsy)
&&=
Assigns if current value is truthy
Update existing truthy value
x &&= 10 (assigns if x is truthy)
??=
Assigns only if null/undefined
Set default only for null/undefined
x ??= 10 (assigns if x is null/undefined)
Example: Logical assignment operators
// ||= Logical OR Assignment
// Assigns if left side is falsy
let a = 0;
a ||= 10; // a is 10 (0 is falsy)
let b = 5;
b ||= 10; // b is still 5 (5 is truthy)
let c = "";
c ||= "default"; // c is "default" ("" is falsy)
// &&= Logical AND Assignment
// Assigns if left side is truthy
let x = 5;
x &&= 10; // x is 10 (5 is truthy)
let y = 0;
y &&= 10; // y is still 0 (0 is falsy, no assignment)
let z = null;
z &&= 10; // z is still null (null is falsy)
// ??= Nullish Assignment
// Assigns only if null or undefined
let p = 0;
p ??= 10; // p is still 0 (0 is not null/undefined)
let q = null;
q ??= 10; // q is 10 (null)
let r = undefined;
r ??= 10; // r is 10 (undefined)
let s = false;
s ??= true; // s is still false (false is not null/undefined)
// Practical use cases
// 1. Object property defaults with ??=
const config = {
timeout: 0, // 0 is valid
retry: null // null should be replaced
};
config.timeout ??= 3000; // Still 0 (not null/undefined)
config.retry ??= 3; // Now 3 (was null)
config.newProp ??= "default"; // "default" (was undefined)
console.log(config);
// { timeout: 0, retry: 3, newProp: "default" }
// 2. Accumulation with &&=
let total = 100;
let applyDiscount = true;
total &&= total * 0.9; // Apply 10% discount if total exists
console.log(total); // 90
let nullTotal = null;
nullTotal &&= nullTotal * 0.9; // No operation (nullTotal is falsy)
console.log(nullTotal); // null (unchanged)
// 3. Cache pattern with ||=
let cache = {};
function getData(key) {
// Set cache if not already set
cache[key] ||= expensiveOperation(key);
return cache[key];
}
function expensiveOperation(key) {
console.log("Computing...");
return key * 2;
}
console.log(getData(5)); // "Computing..." then 10
console.log(getData(5)); // 10 (from cache, no computing)
// 4. Updating objects conditionally
const user = {
name: "Alice",
age: null,
status: undefined
};
user.name ||= "Anonymous"; // Still "Alice" (truthy)
user.age ??= 18; // Now 18 (was null)
user.status ??= "active"; // Now "active" (was undefined)
// 5. Short-circuit: right side not evaluated if condition not met
let count = 0;
let getValue = () => { count++; return 100; };
let x1 = 5;
x1 ||= getValue(); // getValue() NOT called (x1 is truthy)
console.log(count); // 0
let x2 = 0;
x2 ||= getValue(); // getValue() IS called (x2 is falsy)
console.log(count); // 1
Note: Logical assignment operators combine logical operations with assignment, providing
cleaner syntax and short-circuit evaluation (right side only evaluated when assignment occurs).
Section 4 Summary
- Arithmetic operators include +, -, *, /, %, ** (exponentiation);
+ concatenates strings if either operand is string
- Assignment operators support compound forms (+=, -=, *=, etc.) for concise
update operations
- Use strict equality (===, !==) to avoid type coercion bugs; loose equality
(==) performs type conversion
- Logical operators (&&, ||, !) short-circuit and return actual values, not
just booleans; useful for defaults and guards
- Bitwise operators work on 32-bit integers; useful for flags, permissions,
and fast math operations
- Optional chaining (?.) safely accesses nested properties; nullish coalescing (??) provides defaults only for null/undefined
- Logical assignment (||=, &&=, ??=) combines logic with assignment;
short-circuits when assignment unnecessary
5. Control Flow and Loop Constructs
5.1 Conditional Statements (if, switch, ternary)
Statement
Syntax
Use Case
Notes
if
if (condition) { }
Execute block if condition is truthy
Most common conditional
if...else
if (cond) { } else { }
Execute one of two blocks
Binary choice
if...else if...else
if (c1) { } else if (c2) { } else { }
Multiple conditions, evaluated in order
First truthy wins
switch
switch(expr) { case val: break; }
Multiple discrete values, strict equality (===)
Don't forget break
Ternary
condition ? trueVal : falseVal
Inline conditional expression
Returns value, concise
Nested Ternary
c1 ? v1 : c2 ? v2 : v3
Multiple conditions inline
Can be hard to read
Feature
if/else
switch
Ternary
Returns Value
No (statements)
No (statements)
Yes (expression)
Multiple Conditions
✓ Any expression
Single expression, multiple cases
One condition per ternary
Comparison Type
Any truthy/falsy
Strict equality (===)
Any truthy/falsy
Fall-through
N/A
✓ Without break
N/A
Best For
Complex conditions, ranges
Many discrete values
Simple inline conditions
Example: Conditional statements
// if statement
let age = 20;
if (age >= 18) {
console.log("Adult");
}
// if...else
if (age >= 18) {
console.log("Adult");
} else {
console.log("Minor");
}
// if...else if...else
let score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else if (score >= 70) {
console.log("C");
} else {
console.log("F");
}
// switch statement
let day = "Monday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("Weekday");
break; // Important!
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Invalid day");
}
// switch with expressions (modern pattern)
let result = switch(status) { // Not yet in JS, but coming
case 200: "OK";
case 404: "Not Found";
default: "Unknown";
};
// Ternary operator
let message = age >= 18 ? "Adult" : "Minor";
console.log(message);
// Nested ternary
let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
// Ternary with side effects (avoid for readability)
let value = condition ? (count++, doSomething()) : doSomethingElse();
// Multiple ternaries (can be hard to read)
let type = age < 13 ? "child"
: age < 20 ? "teen"
: age < 65 ? "adult"
: "senior";
// Guard clauses (early return pattern)
function processUser(user) {
if (!user) return; // Guard clause
if (!user.isActive) return; // Guard clause
if (!user.hasPermission) return; // Guard clause
// Main logic here
console.log("Processing user...");
}
Warning: Always use break in switch cases to prevent fall-through (unless
intentional). Nested ternaries can reduce readability; use if/else for complex logic.
5.2 Loop Statements (for, while, do-while)
Loop Type
Syntax
When to Use
Characteristics
for
for (init; cond; update) { }
Known iteration count
Initialization, condition, update in header
while
while (condition) { }
Unknown iteration count, condition first
Tests condition before each iteration
do-while
do { } while (condition);
At least one iteration guaranteed
Tests condition after each iteration
Feature
for
while
do-while
Minimum Iterations
0 (can skip entirely)
0 (can skip entirely)
1 (always executes once)
Condition Check
Before each iteration
Before each iteration
After each iteration
Loop Variable Scope
Block-scoped if using let/const
Declared outside loop
Declared outside loop
Best For
Counter-based iteration
Condition-based iteration
Execute-then-check pattern
Example: Classic loop statements
// for loop - most common for arrays/counters
for (let i = 0; i < 5; i++) {
console.log(i); // 0, 1, 2, 3, 4
}
// Multiple variables in for loop
for (let i = 0, j = 10; i < 5; i++, j--) {
console.log(i, j); // (0,10), (1,9), (2,8), (3,7), (4,6)
}
// Infinite loop (condition always true)
// for (;;) {
// console.log("Forever");
// break; // Need break to exit
// }
// while loop - condition-based
let count = 0;
while (count < 5) {
console.log(count);
count++;
}
// while with complex condition
let items = [1, 2, 3, 4, 5];
let i = 0;
while (i < items.length && items[i] !== 3) {
console.log(items[i]);
i++;
}
// Reading input until valid
let input;
while (!input || input.trim() === "") {
// input = prompt("Enter value:"); // Browser example
input = "test"; // Simulated
}
// do-while - at least one iteration
let num = 0;
do {
console.log(num); // Executes once even though num < 1 is false
num++;
} while (num < 1);
// Menu loop pattern
let choice;
do {
// Display menu
console.log("1. Option 1");
console.log("2. Option 2");
console.log("3. Exit");
// choice = getUserInput();
choice = 3; // Simulated
} while (choice !== 3);
// Empty for loop (all work in condition)
let arr = [1, 2, 3];
for (let i = 0; i < arr.length && arr[i]++ < 10;);
console.log(arr); // [2, 3, 4]
// Nested loops
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
console.log(`${i}, ${j}`);
}
}
// Loop with multiple updates
for (let i = 0; i < 10; i += 2) {
console.log(i); // 0, 2, 4, 6, 8
}
// Reverse loop
for (let i = 5; i >= 0; i--) {
console.log(i); // 5, 4, 3, 2, 1, 0
}
Note: Use for for counter-based iteration, while for condition-based,
and do-while when you need at least one iteration before checking the condition.
5.3 Enhanced Loops (for...in, for...of, for await...of)
Loop Type
Syntax
Iterates Over
Returns
Use For
for...in
for (key in obj) { }
Enumerable properties
Property names (strings)
Object properties
for...of ES6
for (val of iterable) { }
Iterable objects
Values
Arrays, strings, Maps, Sets
for await...of ES2018
for await (val of asyncIterable) { }
Async iterables
Resolved promises
Async generators, streams
Feature
for...in
for...of
Works with Objects
✓ Yes (enumerable properties)
✗ No (unless iterable)
Works with Arrays
✓ Yes (indices as strings)
✓ Yes (values directly)
Works with Strings
✓ Yes (indices)
✓ Yes (characters)
Works with Map/Set
✗ No
✓ Yes
Includes Prototype Chain
✓ Yes (inherited properties)
✗ No
Recommended For
Object properties (use with hasOwnProperty)
Iterables (arrays, strings, collections)
Example: Enhanced loops
// for...in - iterates over object properties
const obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
console.log(key, obj[key]); // "a" 1, "b" 2, "c" 3
}
// for...in with arrays (NOT recommended - use for...of)
const arr = [10, 20, 30];
for (let index in arr) {
console.log(index, arr[index]); // "0" 10, "1" 20, "2" 30
console.log(typeof index); // "string" (not number!)
}
// for...in includes inherited properties
Object.prototype.inheritedProp = "inherited";
const obj2 = { own: "value" };
for (let key in obj2) {
console.log(key); // "own", "inheritedProp"
}
// Use hasOwnProperty to filter
for (let key in obj2) {
if (obj2.hasOwnProperty(key)) {
console.log(key); // "own" only
}
}
// for...of - iterates over iterable values (ES6)
const arr2 = [10, 20, 30];
for (let value of arr2) {
console.log(value); // 10, 20, 30 (values, not indices)
}
// for...of with strings
const str = "hello";
for (let char of str) {
console.log(char); // "h", "e", "l", "l", "o"
}
// for...of with Map
const map = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of map) {
console.log(key, value); // "a" 1, "b" 2
}
// for...of with Set
const set = new Set([1, 2, 3]);
for (let value of set) {
console.log(value); // 1, 2, 3
}
// for...of with array methods
const arr3 = [1, 2, 3, 4, 5];
for (let value of arr3.entries()) {
console.log(value); // [0, 1], [1, 2], [2, 3]...
}
for (let [index, value] of arr3.entries()) {
console.log(index, value); // 0 1, 1 2, 2 3...
}
// for await...of - async iteration (ES2018)
async function fetchSequentially() {
const urls = ['url1', 'url2', 'url3'];
// Create async iterable
async function* fetchUrls() {
for (let url of urls) {
yield fetch(url); // Simulated
}
}
// Iterate and wait for each promise
for await (let response of fetchUrls()) {
console.log(await response.text());
}
}
// for await...of with Promise array
async function processPromises() {
const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
for await (let value of promises) {
console.log(value); // 1, 2, 3 (waits for each)
}
}
// Custom iterable with for...of
const customIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index],
done: index++ >= this.data.length
})
};
}
};
for (let value of customIterable) {
console.log(value); // 1, 2, 3
}
Warning: Use for...of for arrays (not for...in). With
for...in on objects, always use hasOwnProperty() to avoid inherited properties.
5.4 Jump Statements (break, continue, return)
Statement
Syntax
Effect
Use In
break
break;
Exits loop or switch immediately
Loops, switch
break label
break labelName;
Exits named loop/block
Nested loops
continue
continue;
Skips to next iteration
Loops only
continue label
continue labelName;
Continues named loop
Nested loops
return
return value;
Exits function with value
Functions only
return (void)
return;
Exits function, returns undefined
Functions only
Statement
break
continue
return
In for loop
✓ Exits loop
✓ Next iteration
✓ Exits function (and loop)
In while loop
✓ Exits loop
✓ Next iteration
✓ Exits function (and loop)
In switch
✓ Exits switch
✗ Syntax error
✓ Exits function (and switch)
In function
✗ Must be in loop/switch
✗ Must be in loop
✓ Exits function
With labels
✓ Can target outer loop
✓ Can target outer loop
✗ Cannot use labels
Example: Jump statements
// break - exit loop immediately
for (let i = 0; i < 10; i++) {
if (i === 5) break; // Exit loop when i is 5
console.log(i); // 0, 1, 2, 3, 4
}
// break in nested loop (only exits inner loop)
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) break; // Exits inner loop only
console.log(`${i},${j}`);
}
}
// Output: 0,0 1,0 2,0
// continue - skip to next iteration
for (let i = 0; i < 5; i++) {
if (i === 2) continue; // Skip when i is 2
console.log(i); // 0, 1, 3, 4
}
// continue with condition
const arr = [1, 2, 3, 4, 5];
for (let num of arr) {
if (num % 2 === 0) continue; // Skip even numbers
console.log(num); // 1, 3, 5
}
// return - exit function
function findValue(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i; // Exit function, return index
}
}
return -1; // Not found
}
console.log(findValue([1, 2, 3], 2)); // 1
console.log(findValue([1, 2, 3], 5)); // -1
// return without value (returns undefined)
function logMessage(msg) {
if (!msg) return; // Early exit
console.log(msg);
}
// Multiple return points (guard clauses)
function validateUser(user) {
if (!user) return { valid: false, error: "No user" };
if (!user.name) return { valid: false, error: "No name" };
if (!user.email) return { valid: false, error: "No email" };
return { valid: true };
}
// break in switch
function getDayType(day) {
switch (day) {
case 'Mon':
case 'Tue':
case 'Wed':
case 'Thu':
case 'Fri':
return 'Weekday';
// No break needed after return
case 'Sat':
case 'Sun':
return 'Weekend';
default:
return 'Invalid';
}
}
// Finding first match
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 }
];
let found;
for (let user of users) {
if (user.age > 28) {
found = user;
break; // Stop after finding first match
}
}
console.log(found); // Bob
// Skip invalid items
const data = [1, null, 2, undefined, 3, NaN, 4];
for (let item of data) {
if (item == null || Number.isNaN(item)) continue;
console.log(item); // 1, 2, 3, 4
}
Note: Use break to exit loops early, continue to skip iterations, and
return to exit functions. For nested loops, consider labeled statements for more control.
5.5 Exception Handling (try, catch, finally, throw)
Keyword
Purpose
Required
Description
try
Execute code that may throw
✓ Yes
Contains code to be tested for errors
catch
Handle exceptions
✓ Yes (or finally)
Executes if error thrown in try block
finally
Cleanup code
✗ Optional
Always executes (even if error/return)
throw
Throw exception
N/A
Creates and throws error object
Error Type
When Thrown
Example
Error
Generic error (base class)
new Error("message")
SyntaxError
Invalid JavaScript syntax
eval("{")
ReferenceError
Invalid reference/undeclared variable
console.log(undeclaredVar)
TypeError
Value not of expected type
null.property
RangeError
Value not in allowed range
new Array(-1)
URIError
Invalid URI encoding/decoding
decodeURI("%")
Example: Exception handling
// Basic try-catch
try {
let result = riskyOperation();
console.log(result);
} catch (error) {
console.error("Error:", error.message);
}
// Accessing error properties
try {
throw new Error("Something went wrong");
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace
}
// try-catch-finally
try {
console.log("Try block");
// throw new Error("Test");
} catch (error) {
console.log("Catch block:", error.message);
} finally {
console.log("Finally always runs");
}
// finally runs even with return
function testFinally() {
try {
return "from try";
} catch (error) {
return "from catch";
} finally {
console.log("Finally executed"); // Runs before return
}
}
console.log(testFinally()); // "Finally executed", then "from try"
// Throwing errors
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
try {
console.log(divide(10, 0));
} catch (error) {
console.log("Caught:", error.message);
}
// Throwing custom errors
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateAge(age) {
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
if (age > 150) {
throw new ValidationError("Age too high");
}
return true;
}
try {
validateAge(-5);
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation error:", error.message);
} else {
console.log("Other error:", error);
}
}
// Nested try-catch
try {
try {
throw new Error("Inner error");
} catch (innerError) {
console.log("Inner catch:", innerError.message);
throw new Error("Outer error"); // Re-throw or new error
}
} catch (outerError) {
console.log("Outer catch:", outerError.message);
}
// Catching specific error types
try {
// Some operation
JSON.parse("invalid json");
} catch (error) {
if (error instanceof SyntaxError) {
console.log("JSON syntax error");
} else if (error instanceof TypeError) {
console.log("Type error");
} else {
console.log("Unknown error");
}
}
// Async error handling
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch error:", error);
throw error; // Re-throw for caller to handle
} finally {
console.log("Cleanup operations");
}
}
// Error without catch (just finally)
try {
console.log("Executing...");
} finally {
console.log("Cleanup"); // Valid: try-finally without catch
}
// Throwing non-Error objects (possible but not recommended)
try {
throw "String error"; // Works but not best practice
throw { code: 500 }; // Works but no stack trace
throw 42; // Works but no context
} catch (error) {
console.log(typeof error); // Could be anything
}
Warning: Always catch errors for operations that may fail (network, parsing, etc.). Use
finally for cleanup that must run regardless of success/failure.
5.6 Labels and Labeled Statements
Feature
Syntax
Use With
Purpose
Label Definition
labelName: statement
Any statement/block
Names a statement for reference
Labeled break
break labelName;
Loops, blocks
Exits named loop/block
Labeled continue
continue labelName;
Loops only
Continues named loop
Use Case
Without Labels
With Labels
Break from nested loop
Only breaks inner loop
Can break outer loop directly
Continue outer loop
Requires flag variable
Direct continue to outer loop
Exit multiple levels
Multiple break statements
Single labeled break
Code clarity
Complex control flow
Explicit jump targets
Example: Labeled statements
// Labeled break - exit outer loop
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outer; // Breaks out of outer loop
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 0,1 0,2 1,0
// Without label (only breaks inner loop)
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break; // Only breaks inner loop
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 0,1 0,2 1,0 2,0 2,1 2,2
// Labeled continue - continue outer loop
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) {
continue outer; // Continues outer loop, skips rest of inner
}
console.log(`${i},${j}`);
}
}
// Output: 0,0 1,0 2,0
// Finding element in 2D array
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
let target = 5;
let found = false;
search: for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === target) {
console.log(`Found at [${i}][${j}]`);
found = true;
break search; // Exit both loops
}
}
}
// Multiple nested loops with labels
outer: for (let i = 0; i < 2; i++) {
middle: for (let j = 0; j < 2; j++) {
inner: for (let k = 0; k < 2; k++) {
if (i === 1) break outer; // Exit all loops
if (j === 1) break middle; // Exit middle and inner
if (k === 1) break inner; // Exit only inner
console.log(`${i},${j},${k}`);
}
}
}
// Labeled block (not loop)
blockLabel: {
console.log("Start");
if (someCondition) {
break blockLabel; // Exit block early
}
console.log("This may not execute");
}
console.log("After block");
// Complex search with labeled break
const data = [
{ id: 1, items: [10, 20, 30] },
{ id: 2, items: [40, 50, 60] },
{ id: 3, items: [70, 80, 90] }
];
let searchValue = 50;
let result = null;
dataSearch: for (let group of data) {
for (let item of group.items) {
if (item === searchValue) {
result = { groupId: group.id, value: item };
break dataSearch; // Found, exit all loops
}
}
}
console.log(result); // { groupId: 2, value: 50 }
// Alternative to labeled break (using function return)
function findInMatrix(matrix, target) {
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === target) {
return [i, j]; // Return exits function (simpler)
}
}
}
return null;
}
// Labeled continue with condition
outer: for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (i + j > 5) {
continue outer; // Skip to next outer iteration
}
console.log(`Sum: ${i + j}`);
}
}
Note: Labeled statements are useful for complex nested loops but can reduce readability.
Consider refactoring into separate functions with return as a cleaner alternative.
Section 5 Summary
- Conditionals: Use
if/else for complex logic,
switch for discrete values, ternary for inline expressions
- Loops:
for (known count), while
(condition-based), do-while (at least once)
- Enhanced loops:
for...of for iterables (arrays, strings),
for...in for object properties, for await...of for async
- Jump statements:
break exits loops/switch,
continue skips to next iteration, return exits functions
- Exception handling:
try-catch-finally for error handling;
throw to raise errors; finally always executes
- Labels: Enable break/continue to target outer loops; useful for nested
structures but consider function refactoring
6. Functions and Functional Programming
6.1 Function Declaration and Expression Syntax
Function Type
Syntax
Hoisting
Use Case
Function Declaration
function name(params) { body }
✓ Fully hoisted (can call before declaration)
Top-level functions, utility functions requiring hoisting
Function Expression
const name = function(params) { body };
✗ Variable hoisted but undefined (TDZ for let/const)
Conditional function creation, callbacks, function as data
Named Function Expression
const f = function name(params) { body };
✗ Not hoisted; name only visible inside function
Recursion in expressions, better stack traces
Arrow Function ES6
const name = (params) => expression
✗ Variable hoisting behavior
Short callbacks, lexical this binding, concise syntax
Method Definition ES6
{ methodName(params) { body } }
✗ Part of object/class definition
Object methods, class methods with concise syntax
Generator Function ES6
function* name(params) { yield value }
✓ Hoisted like regular functions
Iterators, lazy evaluation, pausable execution
Async Function ES8
async function name(params) { await promise }
✓ Hoisted (declaration) / ✗ (expression)
Asynchronous operations with cleaner syntax
Example: Function declaration vs expression
// Function Declaration - hoisted, can call before definition
console.log(add(2, 3)); // Works: 5
function add(a, b) {
return a + b;
}
// Function Expression - not hoisted
// console.log(subtract(5, 2)); // Error: Cannot access before initialization
const subtract = function(a, b) {
return a - b;
};
// Named Function Expression - name visible only inside
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1); // 'fact' accessible here
};
console.log(factorial(5)); // 120
// console.log(fact(5)); // Error: fact is not defined
// Arrow function - concise syntax
const multiply = (a, b) => a * b;
const square = x => x * x; // Single param, no parens needed
const greet = () => 'Hello'; // No params
Best Practice: Use function declarations for top-level utility functions that need hoisting.
Use arrow functions for callbacks and methods that don't need their own this. Use function
expressions when functions are conditional or need to be treated as values.
6.2 Arrow Functions and Lexical this Binding
Feature
Arrow Function
Regular Function
Impact
this Binding
Lexical (from enclosing scope)
Dynamic (based on call)
No .bind() needed for callbacks
arguments Object
✗ Not available (use rest params)
✓ Available
Use ...args for arrow functions
new Operator
✗ Cannot be constructor
✓ Can be constructor
Arrow functions have no [[Construct]]
prototype Property
✗ No prototype
✓ Has prototype
Lighter memory footprint for arrows
super Keyword
✗ Cannot use super
✓ Can use in methods
Use regular functions for class methods needing super
yield Keyword
✗ Cannot be generator
✓ Can yield
Use function* for generators
Syntax Form
Example
When to Use
No Parameters
() => expression
Functions with no inputs
Single Parameter
x => expression
Unary functions (map, filter callbacks)
Multiple Parameters
(x, y) => expression
Binary/multi-parameter functions
Expression Body
x => x * 2
Single expression, implicit return
Block Body
x => { return x * 2; }
Multiple statements, explicit return
Object Literal Return
x => ({ value: x })
Returning object (wrap in parentheses)
Example: Lexical this binding in practice
// Problem with regular functions - this binding changes
function Timer() {
this.seconds = 0;
// Regular function - 'this' is window/undefined in strict mode
setInterval(function() {
this.seconds++; // ✗ Wrong 'this'
console.log(this.seconds); // NaN
}, 1000);
}
// Solution 1: Arrow function - lexical 'this'
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // ✓ Correct 'this' from Timer
console.log(this.seconds); // 1, 2, 3...
}, 1000);
}
// Solution 2: Old approach with .bind() (before ES6)
function Timer() {
this.seconds = 0;
setInterval(function() {
this.seconds++;
}.bind(this), 1000); // Explicitly bind 'this'
}
// Class methods with arrow functions
class Counter {
count = 0;
// Arrow function as class field - 'this' always bound to instance
increment = () => {
this.count++;
}
// Regular method - 'this' depends on how it's called
decrement() {
this.count--;
}
}
const counter = new Counter();
const inc = counter.increment;
const dec = counter.decrement;
inc(); // ✓ Works - arrow function has lexical 'this'
dec(); // ✗ Error - 'this' is undefined (not bound)
Example: Arrow function syntax variations
// Expression body - implicit return
const double = x => x * 2;
const sum = (a, b) => a + b;
// Block body - explicit return needed
const greet = name => {
const message = `Hello, ${name}!`;
return message.toUpperCase();
};
// Returning object literal - wrap in parentheses
const makePerson = (name, age) => ({ name, age });
const point = (x, y) => ({ x: x, y: y });
// Array methods with arrows
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
// Chaining with concise arrows
const result = [1, 2, 3, 4, 5]
.filter(x => x % 2 === 0)
.map(x => x * x)
.reduce((sum, x) => sum + x, 0); // 20
Warning: Don't use arrow functions as object methods or event handlers where you need dynamic
this. Arrow functions cannot be used as constructors with new. They don't have
arguments object - use rest parameters instead.
6.3 Function Parameters (default, rest, destructuring)
Parameter Type
Syntax
Description
Example
Required Parameters
function f(a, b)
Traditional positional parameters; undefined if not provided
f(1, 2)
Default Parameters ES6
function f(a = 0, b = 1)
Provides default value if undefined (not for null)
f() // a=0, b=1
Rest Parameters ES6
function f(...args)
Collects remaining arguments into array; must be last
f(1,2,3) // args=[1,2,3]
Destructuring (Array) ES6
function f([a, b])
Extracts values from array argument by position
f([10, 20])
Destructuring (Object) ES6
function f({x, y})
Extracts properties from object argument by name
f({x:1, y:2})
Nested Destructuring ES6
function f({a: {b}})
Extracts nested properties from deep object structure
f({a: {b: 5}})
Default + Destructuring ES6
function f({x = 0} = {})
Combines destructuring with defaults for object properties
f() // x=0
Pattern
Example
Behavior
Default with Expression
f(a = computeDefault())
Expression evaluated only if parameter is undefined
Using Previous Params
f(a, b = a * 2)
Default can reference earlier parameters
Rest with Destructuring
f([first, ...rest])
Combine array destructuring with rest collection
Rename in Destructuring
f({x: width, y: height})
Extract and rename properties simultaneously
Example: Default parameters
// Basic default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Bob', 'Hi')); // "Hi, Bob!"
// Default only applies to undefined, not null
console.log(greet(null)); // "Hello, null!" (null !== undefined)
console.log(greet(undefined, 'Hey')); // "Hey, Guest!" (undefined triggers default)
// Default expressions evaluated at call time
let counter = 0;
function f(x = counter++) {
return x;
}
console.log(f()); // 0 (counter becomes 1)
console.log(f()); // 1 (counter becomes 2)
console.log(f(10)); // 10 (default not used, counter stays 2)
// Using previous parameters in defaults
function createRect(width, height = width) {
return { width, height }; // height defaults to width (square)
}
console.log(createRect(5)); // { width: 5, height: 5 }
console.log(createRect(5, 10)); // { width: 5, height: 10 }
Example: Rest parameters
// Rest parameters collect remaining arguments into array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum()); // 0 (empty array)
// Combine regular params with rest
function multiply(multiplier, ...numbers) {
return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
// Rest must be last parameter
// function invalid(...args, last) {} // ✗ SyntaxError
// Rest vs arguments object
function oldWay() {
// arguments is array-like, not real array
const args = Array.from(arguments);
return args.reduce((sum, n) => sum + n, 0);
}
function newWay(...args) {
// args is real array with all array methods
return args.reduce((sum, n) => sum + n, 0);
}
// Arrow functions don't have arguments, use rest
const arrowSum = (...nums) => nums.reduce((s, n) => s + n, 0);
Example: Destructuring parameters
// Object destructuring in parameters
function createUser({ name, age, email = 'none@example.com' }) {
return { name, age, email };
}
createUser({ name: 'Alice', age: 30 });
// { name: 'Alice', age: 30, email: 'none@example.com' }
// Array destructuring in parameters
function sumFirstTwo([a, b]) {
return a + b;
}
console.log(sumFirstTwo([10, 20, 30])); // 30
// Nested destructuring
function displayPoint({ coords: { x, y }, label = 'Point' }) {
return `${label}: (${x}, ${y})`;
}
displayPoint({ coords: { x: 10, y: 20 } }); // "Point: (10, 20)"
// Destructuring with rest
function getFirstAndRest([first, ...rest]) {
return { first, rest };
}
console.log(getFirstAndRest([1, 2, 3, 4]));
// { first: 1, rest: [2, 3, 4] }
// Rename while destructuring
function printDimensions({ width: w, height: h }) {
console.log(`Width: ${w}, Height: ${h}`);
}
printDimensions({ width: 100, height: 200 });
// Default for entire destructured object
function config({ host = 'localhost', port = 8080 } = {}) {
return `${host}:${port}`;
}
console.log(config()); // "localhost:8080"
console.log(config({ host: 'example.com' })); // "example.com:8080"
6.4 Higher-Order Functions and Callbacks
Concept
Definition
Common Use Cases
Higher-Order Function (HOF)
Function that takes function(s) as argument(s) and/or returns a function
map, filter, reduce, addEventListener, middleware
Callback Function
Function passed as argument to be executed later (synchronous or asynchronous)
Event handlers, async operations, array methods
Function Returning Function
HOF that creates and returns new function (function factory)
Configuration, currying, closures, partial application
Function Composition
Combining functions where output of one is input to another
Data transformation pipelines, functional programming
Built-in HOF
Purpose
Callback Signature
Returns
Array.map()
Transform each element
(element, index, array) => newValue
New array (same length)
Array.filter()
Select elements by condition
(element, index, array) => boolean
New array (≤ original length)
Array.reduce()
Aggregate to single value
(accumulator, element, index, array) => newAcc
Single accumulated value
Array.forEach()
Execute side effects
(element, index, array) => void
undefined (no return value)
Array.find()
Find first matching element
(element, index, array) => boolean
Element or undefined
Array.some()
Test if any element passes
(element, index, array) => boolean
boolean
Array.every()
Test if all elements pass
(element, index, array) => boolean
boolean
Example: Higher-order functions
// Function that takes function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // Logs: 0, 1, 2
// Function that returns function (function factory)
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Function composition
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
const addOne = x => x + 1;
const square = x => x * x;
const addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(4)); // 25 (5²)
// Practical HOF: logger wrapper
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // Logs call info, returns 5
Example: Array HOF methods in action
const users = [
{ id: 1, name: 'Alice', age: 25, active: true },
{ id: 2, name: 'Bob', age: 30, active: false },
{ id: 3, name: 'Charlie', age: 35, active: true }
];
// map - transform data
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']
// filter - select subset
const activeUsers = users.filter(user => user.active);
// [Alice, Charlie]
// reduce - aggregate
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90
// Chaining HOFs
const result = users
.filter(user => user.active)
.map(user => user.name)
.map(name => name.toUpperCase());
// ['ALICE', 'CHARLIE']
// find - first match
const user = users.find(u => u.age > 25);
// { id: 2, name: 'Bob', age: 30, active: false }
// some - any match
const hasInactive = users.some(u => !u.active); // true
// every - all match
const allActive = users.every(u => u.active); // false
// Complex reduce example
const grouped = users.reduce((groups, user) => {
const key = user.active ? 'active' : 'inactive';
groups[key] = groups[key] || [];
groups[key].push(user);
return groups;
}, {});
// { active: [Alice, Charlie], inactive: [Bob] }
Best Practice: Prefer declarative HOFs (map, filter, reduce) over imperative loops for data
transformation. Chain operations for readability. Use arrow functions for concise callbacks. Remember that HOFs
enable code reuse and abstraction.
6.5 Closures and Function Scope Management
Concept
Definition
Key Characteristics
Closure
Function that retains access to variables from its outer (enclosing) scope even after outer function has
returned
Lexical scoping, persistent state, data privacy
Lexical Environment
Internal structure holding variable bindings and reference to outer environment
Created at function execution, forms scope chain
Scope Chain
Chain of lexical environments searched for variable resolution
Inner to outer traversal, stops at first match
Private Variables
Variables accessible only through closures, not directly from outside
Encapsulation, data hiding, module pattern
Closure Pattern
Use Case
Example Pattern
Function Factory
Create specialized functions with preset parameters
function makeAdder(x) { return y => x + y; }
Private State
Encapsulate data with controlled access
function counter() { let n=0; return ()=>++n; }
Module Pattern
Create modules with public/private members
IIFE returning object with methods accessing private vars
Event Handlers
Preserve context in async callbacks
Handler functions accessing outer scope variables
Memoization
Cache function results using closure-stored cache
Closure over cache object for recursive functions
Example: Basic closure mechanics
// Simple closure - inner function accesses outer variable
function outer() {
let count = 0; // Variable in outer scope
function inner() {
count++; // Accesses count from outer scope
return count;
}
return inner;
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls through closure
// Multiple closures over same variable
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const c1 = makeCounter();
const c2 = makeCounter(); // Separate closure, separate count
c1.increment();
c1.increment();
console.log(c1.value()); // 2
console.log(c2.value()); // 0 (independent)
Example: Practical closure patterns
// Function factory with closure
function multiplier(factor) {
return function(number) {
return number * factor; // 'factor' remembered via closure
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Private variables pattern
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
// No direct access to 'balance' from outside
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
console.log(account.balance); // undefined (private)
console.log(account.getBalance()); // 120
// Memoization with closure
function memoize(fn) {
const cache = {}; // Closure over cache
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('Cached');
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const slowFunction = (n) => {
console.log('Computing...');
return n * 2;
};
const fast = memoize(slowFunction);
fast(5); // Computing... 10
fast(5); // Cached 10
Common Pitfall: Closures in loops. Using var creates single binding shared across
iterations. Solution: use let (creates new binding per iteration) or IIFE to capture value.
// ✗ Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all closures see same 'i')
// ✓ Solution 1: Use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 (each closure gets own 'i')
// ✓ Solution 2: IIFE (old approach)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Logs: 0, 1, 2
6.6 IIFE Patterns and Module Creation
Pattern
Syntax
Purpose
Use Case
Basic IIFE
(function() { })();
Execute function immediately, create private scope
Avoid global pollution, one-time initialization
Alternative Syntax
(function() { }());
Same as basic IIFE (different grouping)
Style preference, same behavior
Arrow IIFE ES6
(() => { })();
Concise IIFE with lexical this
Modern alternative, shorter syntax
Named IIFE
(function name() { })();
Self-reference for recursion, better debugging
Recursive IIFEs, stack traces
IIFE with Parameters
(function(x) { })(value);
Pass values into IIFE scope
Dependency injection, value capture
IIFE Returning Value
const x = (function() { return val; })();
Initialize variable with complex logic
Complex initialization, module pattern
Module Pattern (IIFE)
const mod = (function() { return {...}; })();
Create module with public API, private state
Pre-ES6 modules, encapsulation
Example: IIFE basics and variations
// Basic IIFE - executes immediately
(function() {
const message = 'Hello from IIFE';
console.log(message);
})(); // Logs: Hello from IIFE
// 'message' not accessible here (private scope)
// IIFE with parameters
(function(name) {
console.log(`Hello, ${name}!`);
})('Alice'); // Hello, Alice!
// IIFE returning value
const config = (function() {
const privateKey = 'secret123';
return {
apiUrl: 'https://api.example.com',
timeout: 5000,
getKey: () => privateKey // Controlled access
};
})();
console.log(config.apiUrl); // 'https://api.example.com'
console.log(config.privateKey); // undefined (private)
// Arrow function IIFE (ES6+)
(() => {
const temp = 'temporary variable';
console.log(temp);
})();
// Named IIFE for recursion
(function factorial(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
})(5); // 120
// IIFE for variable capture in loops (pre-let solution)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => console.log(index), 100);
})(i); // Capture current value of i
}
Example: Module pattern with IIFE
// Classic Module Pattern
const Calculator = (function() {
// Private variables and functions
let history = [];
function log(operation, result) {
history.push({ operation, result, timestamp: Date.now() });
}
// Public API
return {
add(a, b) {
const result = a + b;
log('add', result);
return result;
},
subtract(a, b) {
const result = a - b;
log('subtract', result);
return result;
},
getHistory() {
return [...history]; // Return copy, not reference
},
clearHistory() {
history = [];
}
};
})();
Calculator.add(5, 3); // 8
Calculator.subtract(10, 4); // 6
console.log(Calculator.getHistory()); // Array of operations
console.log(Calculator.history); // undefined (private)
// Revealing Module Pattern
const Counter = (function() {
let count = 0;
function increment() { return ++count; }
function decrement() { return --count; }
function getValue() { return count; }
function reset() { count = 0; }
// Reveal selected functions
return {
increment,
decrement,
getValue,
reset
};
})();
Counter.increment(); // 1
Counter.increment(); // 2
console.log(Counter.getValue()); // 2
// Module with dependencies
const UserManager = (function($, _) {
// Use jQuery ($) and lodash (_) passed as parameters
function getUsers() {
return _.map(users, user => user.name);
}
return { getUsers };
})(jQuery, lodash); // Inject dependencies
Modern Alternative: ES6 modules (import/export) have largely replaced IIFE-based
modules. However, IIFEs remain useful for: creating private scope, avoiding global pollution, one-time
initialization code, and browser compatibility when modules aren't available.
6.7 Function Methods (call, apply, bind)
Method
Syntax
Arguments
Returns
When Called
call()
fn.call(thisArg, arg1, arg2, ...)
Individual arguments
Function result
Immediately
apply()
fn.apply(thisArg, [args])
Array of arguments
Function result
Immediately
bind()
fn.bind(thisArg, arg1, ...)
Individual arguments (partial)
New bound function
Later (returned fn)
Use Case
Best Method
Why
Set 'this' and call now
call()
Known number of arguments, immediate execution
Set 'this' with array args
apply()
Arguments already in array, or use Math.max/min with arrays
Set 'this' for later use
bind()
Event handlers, callbacks, partial application
Partial application
bind()
Pre-fill some arguments, returns reusable function
Borrowing methods
call() or apply()
Use array methods on array-like objects
Example: call() and apply()
const person = {
name: 'Alice',
greet(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
};
console.log(person.greet('Hello', '!')); // "Hello, I'm Alice!"
// call() - set 'this' and pass individual arguments
const otherPerson = { name: 'Bob' };
console.log(person.greet.call(otherPerson, 'Hi', '.'));
// "Hi, I'm Bob."
// apply() - set 'this' and pass arguments as array
console.log(person.greet.apply(otherPerson, ['Hey', '...']));
// "Hey, I'm Bob..."
// Practical: borrowing array methods for array-like objects
function sumAll() {
// 'arguments' is array-like, not real array
// Borrow reduce() from Array
return Array.prototype.reduce.call(arguments, (sum, n) => sum + n, 0);
}
console.log(sumAll(1, 2, 3, 4)); // 10
// Math.max with apply (pre-spread operator)
const numbers = [5, 2, 9, 1, 7];
const max = Math.max.apply(null, numbers); // 9
// Modern: Math.max(...numbers)
// Invoking function with specific context
function introduce() {
return `My name is ${this.name} and I'm ${this.age}`;
}
const user = { name: 'Charlie', age: 30 };
console.log(introduce.call(user));
// "My name is Charlie and I'm 30"
Example: bind() for permanent binding
const obj = {
value: 42,
getValue() {
return this.value;
}
};
// Problem: 'this' lost when method assigned to variable
const getValue = obj.getValue;
console.log(getValue()); // undefined ('this' is window/undefined)
// Solution: bind() creates new function with fixed 'this'
const boundGetValue = obj.getValue.bind(obj);
console.log(boundGetValue()); // 42
// Practical: event handlers
class Button {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
handleClick() {
this.clickCount++;
console.log(`${this.label} clicked ${this.clickCount} times`);
}
}
const btn = new Button('Submit');
// Without bind, 'this' would be the button element, not our object
document.querySelector('button').addEventListener('click',
btn.handleClick.bind(btn)
);
// Partial application with bind()
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // Pre-fill first argument
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Binding with setTimeout
const timer = {
seconds: 0,
start() {
// bind() ensures 'this' refers to timer object
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000);
// Modern alternative: arrow function (lexical 'this')
// setInterval(() => { this.seconds++; }, 1000);
}
};
// Multiple bind() calls - first one wins
function log() { console.log(this.value); }
const obj1 = { value: 1 };
const obj2 = { value: 2 };
const bound1 = log.bind(obj1);
const bound2 = bound1.bind(obj2); // Has no effect
bound2(); // 1 (not 2 - can't rebind)
Modern Context: Arrow functions (=>) with lexical this have
reduced the need for bind() in many cases. However, call(), apply(), and
bind() remain essential for: method borrowing, explicit context control, partial application, and
working with legacy code.
6.8 Recursion and Tail Call Optimization
Concept
Definition
Characteristics
Recursion
Function calling itself to solve smaller subproblems
Base case + recursive case; elegant for tree/graph problems
Base Case
Condition that stops recursion without further calls
Prevents infinite recursion; returns direct result
Recursive Case
Logic that breaks problem down and calls function recursively
Progress toward base case; must modify parameters
Call Stack
Stack storing execution contexts for function calls
Each recursive call adds frame; limited size (stack overflow risk)
Tail Call
Function call that's the last operation (nothing after return)
Eligible for optimization; no pending operations
Tail Call Optimization (TCO) LIMITED
Reuse stack frame for tail calls instead of creating new ones
ES6 spec, but poor browser support; prevents stack overflow
Pattern
Stack Usage
TCO Eligible
When to Use
Standard Recursion
O(n) stack frames
✗ No (pending operations after call)
Simple problems, small input size
Tail Recursion
O(1) with TCO, O(n) without
✓ Yes (return is direct recursive call)
Large inputs if TCO available
Accumulator Pattern
O(1) with TCO
✓ Yes (pass result through parameters)
Convert standard recursion to tail recursion
Iteration (Loop)
O(1) stack
N/A (no recursion)
Production code, guaranteed stack safety
Example: Basic recursion patterns
// Standard recursion - NOT tail recursive
function factorial(n) {
// Base case
if (n <= 1) return 1;
// Recursive case - operation AFTER recursive call
return n * factorial(n - 1); // ✗ Not tail call (multiplication pending)
}
console.log(factorial(5)); // 120
// Stack: factorial(5) waits for factorial(4)
// factorial(4) waits for factorial(3), etc.
// Tail recursive version - uses accumulator
function factorialTail(n, acc = 1) {
// Base case
if (n <= 1) return acc;
// Recursive case - nothing after recursive call
return factorialTail(n - 1, n * acc); // ✓ Tail call (direct return)
}
console.log(factorialTail(5)); // 120
// With TCO: reuses same stack frame
// Fibonacci - standard recursion (exponential time!)
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // Two recursive calls
}
// Very slow for large n: fib(40) takes seconds
// Fibonacci - tail recursive with two accumulators
function fibTail(n, a = 0, b = 1) {
if (n === 0) return a;
return fibTail(n - 1, b, a + b); // Tail call
}
console.log(fibTail(40)); // Fast even for large n
// Array sum - standard recursion
function sumArray(arr) {
if (arr.length === 0) return 0;
return arr[0] + sumArray(arr.slice(1)); // ✗ Not tail call
}
// Array sum - tail recursive
function sumArrayTail(arr, acc = 0) {
if (arr.length === 0) return acc;
return sumArrayTail(arr.slice(1), acc + arr[0]); // ✓ Tail call
}
Example: Converting to tail recursion
// Problem: countdown - NOT tail recursive
function countdown(n) {
if (n < 0) return;
console.log(n);
countdown(n - 1);
console.log('Done', n); // ✗ Operation after recursive call
}
// Solution: tail recursive countdown
function countdownTail(n) {
if (n < 0) return;
console.log(n);
return countdownTail(n - 1); // ✓ Tail call
}
// Problem: power - NOT tail recursive
function power(base, exp) {
if (exp === 0) return 1;
return base * power(base, exp - 1); // ✗ Multiplication pending
}
// Solution: tail recursive with accumulator
function powerTail(base, exp, acc = 1) {
if (exp === 0) return acc;
return powerTail(base, exp - 1, acc * base); // ✓ Tail call
}
// Tree traversal - naturally recursive
function sumTree(node) {
if (!node) return 0;
return node.value
+ sumTree(node.left)
+ sumTree(node.right);
// Not tail recursive but appropriate for trees
}
// Tail recursive tree traversal (using continuation)
function sumTreeTail(node, cont = x => x) {
if (!node) return cont(0);
return sumTreeTail(node.left, leftSum =>
sumTreeTail(node.right, rightSum =>
cont(node.value + leftSum + rightSum)
)
);
}
Important Limitations:
- TCO Support: Proper tail call optimization is in ES6 spec but only Safari
implements it. Chrome/Firefox/Edge don't support TCO.
- Stack Overflow Risk: Without TCO, even tail-recursive functions will
overflow stack with large inputs (typically 10,000-15,000 calls).
- Production Code: For critical code, use iteration (loops) instead of
recursion to guarantee stack safety.
- Trampoline: Workaround pattern to simulate TCO, but adds complexity.
Example: When to use recursion vs iteration
// ✓ Good use of recursion - tree/graph structures
function findNode(tree, value) {
if (!tree) return null;
if (tree.value === value) return tree;
return findNode(tree.left, value) || findNode(tree.right, value);
}
// ✓ Good use of recursion - divide and conquer
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
if (left > right) return -1;
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
return binarySearch(arr, target, mid + 1, right);
}
// ✗ Bad use of recursion - simple iteration better
function sumToN(n) {
if (n === 0) return 0;
return n + sumToN(n - 1); // Just use n * (n + 1) / 2
}
// ✓ Better: iterative solution
function sumToNIterative(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
// Or: return n * (n + 1) / 2;
}
// Trampoline pattern (TCO workaround)
function trampoline(fn) {
while (typeof fn === 'function') {
fn = fn(); // Keep calling until not a function
}
return fn;
}
function factorialTrampoline(n, acc = 1) {
if (n <= 1) return acc;
return () => factorialTrampoline(n - 1, n * acc); // Return thunk
}
console.log(trampoline(factorialTrampoline(100000))); // No stack overflow
Section 6 Summary
- Function types: Declarations (hoisted), expressions (not hoisted), arrows
(lexical this), generators (yield), async (await)
- Arrow functions: Concise syntax, lexical
this, no
arguments/new/prototype, ideal for callbacks
- Parameters: Default values, rest (
...args), destructuring
(array/object), combine for flexible APIs
- Higher-order functions: Take/return functions; enables map/filter/reduce,
composition, abstraction
- Closures: Functions retaining outer scope access; enables private state,
factories, memoization
- IIFE: Immediately-invoked functions; creates private scope, module pattern
(pre-ES6 modules)
- call/apply/bind: Control
this context; call/apply execute now,
bind returns new function
- Recursion: Self-calling functions; elegant for trees/graphs; tail recursion
+ TCO prevents stack overflow (limited support)
7. Objects and Property Management
7.1 Object Literal Syntax and Computed Properties
Feature
Syntax
ES Version
Description
Basic Object Literal
{ key: value }
ES3
Traditional property definition with static keys
Property Shorthand ES6
{ name }
ES6
Equivalent to { name: name } when variable name matches key
Method Shorthand ES6
{ method() {} }
ES6
Concise method definition, equivalent to { method: function() {} }
Computed Property ES6
{ [expression]: value }
ES6
Property name computed at runtime from expression
Computed Method ES6
{ [expr]() {} }
ES6
Method name computed dynamically
Spread Properties ES2018
{ ...obj }
ES2018
Copy enumerable properties from source object
Getters/Setters
{ get prop() {}, set prop(v) {} }
ES5
Define accessor properties with custom logic
Example: Modern object literal syntax
// ES6+ object literal features
const name = 'Alice';
const age = 30;
const propKey = 'dynamicKey';
const user = {
// Property shorthand (ES6)
name, // Same as name: name
age, // Same as age: age
// Method shorthand (ES6)
greet() {
return `Hello, I'm ${this.name}`;
},
// Computed property name (ES6)
[propKey]: 'dynamic value',
[`computed_${name}`]: true,
// Computed method name (ES6)
[`get${name}Info`]() {
return `${this.name} is ${this.age}`;
},
// Traditional syntax still works
email: 'alice@example.com',
// Arrow function as property (not a method)
arrowFunc: () => 'arrow',
// Getter and setter
get fullInfo() {
return `${this.name}, ${this.age} years old`;
},
set fullInfo(info) {
[this.name, this.age] = info.split(', ');
}
};
console.log(user.name); // 'Alice'
console.log(user.greet()); // 'Hello, I'm Alice'
console.log(user.dynamicKey); // 'dynamic value'
console.log(user.computed_Alice); // true
console.log(user.getAliceInfo()); // 'Alice is 30'
console.log(user.fullInfo); // 'Alice, 30 years old'
Example: Computed properties in practice
// Dynamic property names
const fields = ['id', 'name', 'email'];
const values = [1, 'Bob', 'bob@example.com'];
// Build object with computed properties
const user = fields.reduce((obj, field, index) => {
obj[field] = values[index];
return obj;
}, {});
// { id: 1, name: 'Bob', email: 'bob@example.com' }
// Using computed properties in literal
const status = 'active';
const config = {
[`is${status.charAt(0).toUpperCase() + status.slice(1)}`]: true
};
// { isActive: true }
// Symbol as computed property
const SECRET = Symbol('secret');
const obj = {
public: 'visible',
[SECRET]: 'hidden' // Property name is a symbol
};
console.log(obj.public); // 'visible'
console.log(obj[SECRET]); // 'hidden'
console.log(Object.keys(obj)); // ['public'] - symbols not enumerable
// Spread properties (ES2018)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'fr', fontSize: 14 }
// userPrefs.lang overwrites defaults.lang
7.2 Property Descriptors and Object.defineProperty
Descriptor Attribute
Type
Default (defineProperty)
Default (Literal)
Description
value
any
undefined
property value
The property's value (data descriptor)
writable
boolean
false
true
Can property value be changed?
enumerable
boolean
false
true
Shows in for...in, Object.keys()?
configurable
boolean
false
true
Can descriptor be changed? Can property be deleted?
get
function
undefined
N/A
Getter function (accessor descriptor)
set
function
undefined
N/A
Setter function (accessor descriptor)
Method
Syntax
Description
Object.defineProperty()
Object.defineProperty(obj, prop, descriptor)
Define or modify single property with full control over attributes
Object.defineProperties()
Object.defineProperties(obj, descriptors)
Define or modify multiple properties at once
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor(obj, prop)
Get descriptor object for single property
Object.getOwnPropertyDescriptors() ES2017
Object.getOwnPropertyDescriptors(obj)
Get descriptors for all own properties
Example: Property descriptors in action
const obj = {};
// Define property with custom descriptor
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false, // Cannot change value
enumerable: true, // Shows in for...in, Object.keys()
configurable: false // Cannot delete or reconfigure
});
obj.name = 'Bob'; // Fails silently (strict mode: TypeError)
console.log(obj.name); // 'Alice' (unchanged)
delete obj.name; // Fails silently (configurable: false)
console.log(obj.name); // 'Alice' (still exists)
// Check property descriptor
const desc = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(desc);
// { value: 'Alice', writable: false, enumerable: true, configurable: false }
// Compare with literal property (all attributes true)
const obj2 = { name: 'Charlie' };
const desc2 = Object.getOwnPropertyDescriptor(obj2, 'name');
console.log(desc2);
// { value: 'Charlie', writable: true, enumerable: true, configurable: true }
Example: Accessor properties with get/set
const person = {
firstName: 'John',
lastName: 'Doe'
};
// Define computed property with getter/setter
Object.defineProperty(person, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true
});
console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
// Define multiple properties at once
const account = {};
Object.defineProperties(account, {
_balance: {
value: 0,
writable: true,
enumerable: false // Private-like (convention)
},
balance: {
get() { return this._balance; },
set(val) {
if (val < 0) throw new Error('Balance cannot be negative');
this._balance = val;
},
enumerable: true
},
currency: {
value: 'USD',
writable: false,
enumerable: true
}
});
console.log(account.balance); // 0
account.balance = 100;
console.log(account.balance); // 100
// account.balance = -50; // Error: Balance cannot be negative
Example: Non-enumerable and non-configurable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false // Won't show in iteration
});
console.log(obj.hidden); // 'secret' (accessible directly)
console.log(Object.keys(obj)); // ['a', 'b'] (hidden not listed)
for (let key in obj) {
console.log(key); // Only logs 'a' and 'b'
}
// Non-configurable property
Object.defineProperty(obj, 'permanent', {
value: 'cannot change',
configurable: false
});
// Cannot reconfigure
try {
Object.defineProperty(obj, 'permanent', {
value: 'new value' // TypeError: cannot redefine
});
} catch (e) {
console.log('Cannot reconfigure');
}
// Cannot delete
delete obj.permanent; // Fails silently
console.log(obj.permanent); // Still exists
// Get all descriptors (ES2017)
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(allDescriptors.hidden);
// { value: 'secret', writable: false, enumerable: false, configurable: false }
Important: Data descriptors (value/writable) and accessor descriptors (get/set) are mutually
exclusive. A property can have value OR get/set, not both. Non-configurable properties
cannot be deleted or have their descriptor changed (except writable can go from true to false).
7.3 Object Methods (Object.keys, Object.values, Object.entries)
Method
Returns
ES Version
Description
Object.keys(obj)
Array of strings
ES5
Own enumerable property names (keys)
Object.values(obj) ES2017
Array of values
ES2017
Own enumerable property values
Object.entries(obj) ES2017
Array of [key, value] pairs
ES2017
Own enumerable properties as key-value pairs
Object.fromEntries(entries) ES2019
Object
ES2019
Create object from array of [key, value] pairs (inverse of entries)
Object.getOwnPropertyNames(obj)
Array of strings
ES5
All own property names (including non-enumerable)
Object.getOwnPropertySymbols(obj) ES6
Array of symbols
ES6
All own symbol properties
Object.hasOwn(obj, prop) ES2022
boolean
ES2022
Safe alternative to hasOwnProperty (recommended)
obj.hasOwnProperty(prop)
boolean
ES3
Check if property is own (not inherited); use Object.hasOwn instead
Comparison
for...in
Object.keys()
Object.getOwnPropertyNames()
Own Properties
✓ Yes
✓ Yes
✓ Yes
Inherited Properties
✓ Yes (from prototype chain)
✗ No
✗ No
Non-enumerable
✗ No
✗ No
✓ Yes
Symbol Properties
✗ No
✗ No
✗ No (use getOwnPropertySymbols)
Example: Object.keys, values, entries
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true
};
// Get all keys
const keys = Object.keys(user);
console.log(keys); // ['id', 'name', 'email', 'active']
// Get all values (ES2017)
const values = Object.values(user);
console.log(values); // [1, 'Alice', 'alice@example.com', true]
// Get key-value pairs (ES2017)
const entries = Object.entries(user);
console.log(entries);
// [['id', 1], ['name', 'Alice'], ['email', 'alice@example.com'], ['active', true]]
// Iterate over entries
Object.entries(user).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// Transform object using entries
const doubled = Object.fromEntries(
Object.entries({ a: 1, b: 2, c: 3 })
.map(([key, value]) => [key, value * 2])
);
console.log(doubled); // { a: 2, b: 4, c: 6 }
// Filter object properties
const activeUsers = Object.fromEntries(
Object.entries({ u1: { active: true }, u2: { active: false } })
.filter(([key, val]) => val.active)
);
// { u1: { active: true } }
Example: Enumerable vs non-enumerable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false
});
// Add symbol property
const sym = Symbol('symbol');
obj[sym] = 'symbol value';
console.log(Object.keys(obj)); // ['a', 'b'] (only enumerable string keys)
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'hidden'] (includes non-enumerable)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(symbol)]
// for...in includes inherited enumerable properties
function Parent() {}
Parent.prototype.inherited = 'from parent';
const child = Object.create(new Parent());
child.own = 'own property';
for (let key in child) {
console.log(key); // Logs: 'own', 'inherited'
}
// Filter to own properties only
for (let key in child) {
if (Object.hasOwn(child, key)) { // ES2022
console.log(key); // Logs: 'own' only
}
}
// Alternative: hasOwnProperty (older)
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // Logs: 'own' only
}
}
Example: Practical use cases
// Count object properties
const obj = { a: 1, b: 2, c: 3 };
const count = Object.keys(obj).length; // 3
// Check if object is empty
const isEmpty = Object.keys(obj).length === 0;
// Merge objects by converting to entries
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.fromEntries([
...Object.entries(obj1),
...Object.entries(obj2)
]);
// { a: 1, b: 3, c: 4 } (obj2 overwrites obj1)
// Convert object to Map
const map = new Map(Object.entries(obj));
// Convert Map to object
const objFromMap = Object.fromEntries(map);
// Swap keys and values
const original = { a: 1, b: 2, c: 3 };
const swapped = Object.fromEntries(
Object.entries(original).map(([k, v]) => [v, k])
);
// { 1: 'a', 2: 'b', 3: 'c' }
// Pick specific properties
function pick(obj, keys) {
return Object.fromEntries(
keys.filter(key => key in obj)
.map(key => [key, obj[key]])
);
}
const user = { id: 1, name: 'Alice', email: 'a@example.com', age: 30 };
const userPreview = pick(user, ['id', 'name']);
// { id: 1, name: 'Alice' }
7.4 Object Creation Patterns (Object.create, constructors)
Pattern
Syntax
Prototype
Use Case
Object Literal
const obj = { }
Object.prototype
Simple data objects, quick creation
Object() Constructor
const obj = new Object()
Object.prototype
Rarely used (literal preferred)
Object.create() ES5
Object.create(proto, descriptors?)
Specified proto
Prototypal inheritance, null prototype objects
Constructor Function
function Ctor() { } + new Ctor()
Ctor.prototype
Pre-ES6 classes, instance creation with shared methods
Factory Function
function create() { return { }; }
Object.prototype (or custom)
Object creation without 'new', private state
Class Syntax ES6
class C { } + new C()
C.prototype
Modern OOP, cleaner syntax over constructors
Method
Description
Returns
Object.create(proto)
Create new object with specified prototype
New object inheriting from proto
Object.create(proto, descriptors)
Create object with prototype and property descriptors
New object with specified properties
Object.create(null)
Create object with no prototype (truly empty)
Object without any inherited properties
Object.setPrototypeOf(obj, proto)
Change prototype of existing object (slow, avoid)
The modified object
Object.getPrototypeOf(obj)
Get prototype of object
Prototype object or null
Example: Object.create() patterns
// Create object with specific prototype
const personProto = {
greet() {
return `Hello, I'm ${this.name}`;
}
};
const person = Object.create(personProto);
person.name = 'Alice';
console.log(person.greet()); // "Hello, I'm Alice"
console.log(Object.getPrototypeOf(person) === personProto); // true
// Create with property descriptors
const user = Object.create(personProto, {
name: {
value: 'Bob',
writable: true,
enumerable: true
},
age: {
value: 30,
writable: true,
enumerable: true
}
});
console.log(user.name); // 'Bob'
console.log(user.greet()); // "Hello, I'm Bob"
// Create object with null prototype (no inherited properties)
const dict = Object.create(null);
dict.key = 'value';
console.log(dict.toString); // undefined (no inherited methods)
console.log(dict.hasOwnProperty); // undefined
// Useful for pure data storage, no prototype pollution
// Compare with literal
const literal = {};
console.log(literal.toString); // [Function: toString] (inherited)
console.log(Object.getPrototypeOf(literal) === Object.prototype); // true
Example: Constructor functions
// Constructor function (pre-ES6 classes)
function Person(name, age) {
// Instance properties (unique per instance)
this.name = name;
this.age = age;
}
// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
return this.age;
};
// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.greet()); // "Hello, I'm Bob"
console.log(alice.greet === bob.greet); // true (shared method)
// Verify prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice instanceof Person); // true
// Forgetting 'new' - common mistake
function SafeConstructor(name) {
// Guard against missing 'new'
if (!(this instanceof SafeConstructor)) {
return new SafeConstructor(name);
}
this.name = name;
}
const instance1 = new SafeConstructor('Alice');
const instance2 = SafeConstructor('Bob'); // Works without 'new'
console.log(instance2 instanceof SafeConstructor); // true
Example: Factory functions
// Factory function - no 'new' needed
function createPerson(name, age) {
// Private variables (closure)
let privateData = 'secret';
// Return object with public interface
return {
name,
age,
greet() {
return `Hello, I'm ${this.name}`;
},
getPrivate() {
return privateData; // Access private via closure
}
};
}
const person1 = createPerson('Alice', 30); // No 'new'
const person2 = createPerson('Bob', 25);
console.log(person1.greet()); // "Hello, I'm Alice"
console.log(person1.getPrivate()); // 'secret'
console.log(person1.privateData); // undefined (truly private)
// Factory with prototypal inheritance
function createUser(name) {
const proto = {
greet() { return `Hello ${this.name}`; }
};
const user = Object.create(proto);
user.name = name;
return user;
}
const user = createUser('Charlie');
console.log(user.greet()); // "Hello Charlie"
// Modern: ES6 class (preferred over constructors)
class PersonClass {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
const personInstance = new PersonClass('Dave', 35);
console.log(personInstance.greet()); // "Hello, I'm Dave"
Best Practice: Modern JavaScript prefers ES6 classes over constructor functions for clarity.
Use factory functions when you need private state via closures or want to avoid new. Use
Object.create(null) for dictionaries/maps to avoid prototype pollution. Avoid
Object.setPrototypeOf() as it's very slow.
7.5 Object Cloning and Merging (Object.assign, spread)
Method
Syntax
Copy Type
Nested Objects
Object.assign() ES6
Object.assign(target, ...sources)
Shallow copy
Copies references (not deep)
Spread Operator ES2018
{ ...obj }
Shallow copy
Copies references (not deep)
JSON.parse/stringify
JSON.parse(JSON.stringify(obj))
Deep copy
✓ Clones nested, but loses functions/symbols/dates
structuredClone() NEW
structuredClone(obj)
Deep copy
✓ Clones nested, preserves types (not functions)
Manual Deep Clone
Recursive function
Deep copy
✓ Full control, handle all types
Feature
Object.assign()
Spread {...}
Differences
Copy enumerable props
✓ Yes
✓ Yes
Both copy own enumerable properties
Trigger setters
✓ Yes
✗ No
assign() invokes setters, spread defines new props
Copy getters/setters
✗ No (executes getter)
✗ No (executes getter)
Both execute getters, copy resulting values
Mutate target
✓ Yes (modifies first arg)
✗ No (creates new object)
assign() mutates, spread creates new
Syntax
Function call
Operator (concise)
Spread is more concise and readable
Example: Shallow copying with Object.assign and spread
const original = {
name: 'Alice',
age: 30,
address: { city: 'NYC', zip: '10001' }
};
// Object.assign - shallow copy
const copy1 = Object.assign({}, original);
copy1.name = 'Bob';
copy1.address.city = 'LA'; // Modifies nested object
console.log(original.name); // 'Alice' (primitive copied)
console.log(original.address.city); // 'LA' (nested reference shared!)
// Spread operator - shallow copy (ES2018)
const copy2 = { ...original };
copy2.age = 25;
copy2.address.zip = '90001'; // Also modifies original!
console.log(original.age); // 30 (primitive copied)
console.log(original.address.zip); // '90001' (nested reference shared!)
// Merge multiple objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { c: 5, d: 6 };
// Using Object.assign
const merged1 = Object.assign({}, obj1, obj2, obj3);
// { a: 1, b: 3, c: 5, d: 6 } (later values overwrite)
// Using spread (more common)
const merged2 = { ...obj1, ...obj2, ...obj3 };
// { a: 1, b: 3, c: 5, d: 6 } (same result)
// Add/override properties while spreading
const enhanced = {
...original,
age: 35, // Override
email: 'alice@example.com' // Add new
};
// { name: 'Alice', age: 35, address: {...}, email: '...' }
Example: Deep cloning techniques
const original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding'],
address: { city: 'NYC', coords: { lat: 40, lng: -74 } },
createdAt: new Date('2024-01-01')
};
// Method 1: JSON (quick but lossy)
const jsonClone = JSON.parse(JSON.stringify(original));
jsonClone.hobbies.push('gaming');
jsonClone.address.city = 'LA';
console.log(original.hobbies); // ['reading', 'coding'] (not affected)
console.log(original.address.city); // 'NYC' (not affected)
console.log(jsonClone.createdAt); // String, not Date! (limitation)
// Method 2: structuredClone() (modern, recommended)
const structClone = structuredClone(original);
structClone.address.coords.lat = 35;
console.log(original.address.coords.lat); // 40 (not affected)
console.log(structClone.createdAt instanceof Date); // true (preserved!)
// Limitations: cannot clone functions
const withFunction = {
data: 'value',
method() { return this.data; }
};
// structuredClone(withFunction); // Error: functions not cloneable
// Method 3: Manual deep clone (full control)
function deepClone(obj, seen = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') return obj;
// Handle circular references
if (seen.has(obj)) return seen.get(obj);
// Handle Date
if (obj instanceof Date) return new Date(obj);
// Handle Array
if (Array.isArray(obj)) {
const arrCopy = [];
seen.set(obj, arrCopy);
obj.forEach((item, i) => {
arrCopy[i] = deepClone(item, seen);
});
return arrCopy;
}
// Handle Object
const objCopy = {};
seen.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], seen);
});
return objCopy;
}
const manualClone = deepClone(original);
manualClone.address.coords.lng = -118;
console.log(original.address.coords.lng); // -74 (not affected)
Example: Object.assign vs spread differences
// Difference 1: Mutating vs creating
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source); // Mutates target
console.log(target); // { a: 1, b: 2 } (modified)
const newObj = { ...target, ...source }; // Creates new object
// target unchanged
// Difference 2: Setters
const obj = {
_value: 0,
set value(v) {
console.log('Setter called:', v);
this._value = v;
}
};
// Object.assign triggers setter
Object.assign(obj, { value: 10 }); // Logs: "Setter called: 10"
// Spread defines new property (doesn't trigger setter)
const copy = { ...obj, value: 20 }; // No setter call
console.log(copy.value); // 20 (simple property, not setter)
// Use case: Default options
function configure(options = {}) {
const defaults = {
timeout: 5000,
retries: 3,
cache: true
};
// Merge with user options (spread preferred)
return { ...defaults, ...options };
}
const config1 = configure({ timeout: 10000 });
// { timeout: 10000, retries: 3, cache: true }
const config2 = configure({ retries: 5, debug: true });
// { timeout: 5000, retries: 5, cache: true, debug: true }
Warning: Both Object.assign() and spread create shallow
copies. Nested objects are copied by reference, not value. Modifying nested properties affects the
original. For true deep copies, use structuredClone() (modern) or implement custom deep clone
logic.
7.6 Property Getters and Setters
Syntax Form
Example
When to Use
Object Literal Getter
{ get prop() { return value; } }
Computed properties, validation on read
Object Literal Setter
{ set prop(val) { this._prop = val; } }
Validation/transformation on write
defineProperty Getter
Object.defineProperty(obj, 'prop', { get() {} })
Add getter to existing object
defineProperty Setter
Object.defineProperty(obj, 'prop', { set(v) {} })
Add setter to existing object
Class Getter
class C { get prop() { } }
ES6 class computed properties
Class Setter
class C { set prop(v) { } }
ES6 class property validation
Use Case
Pattern
Example
Computed Property
Getter derives value from other properties
get fullName() { return this.first + ' ' + this.last; }
Validation
Setter validates before assignment
set age(v) { if (v < 0) throw Error; this._age = v; }
Lazy Initialization
Getter computes value on first access
get data() { return this._data || (this._data = load()); }
Side Effects
Setter triggers additional actions
set value(v) { this._value = v; this.notify(); }
Read-only Property
Getter without setter
get id() { return this._id; } (no setter)
Private Backing Field
Store value in _property, expose via getter/setter
get name() { return this._name; }
Example: Getters and setters in object literals
const person = {
firstName: 'John',
lastName: 'Doe',
_age: 30, // Backing field (convention: underscore = private-like)
// Getter: computed property
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter: parse and set multiple properties
set fullName(name) {
const parts = name.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
},
// Getter with validation/transformation
get age() {
return this._age;
},
// Setter with validation
set age(value) {
if (typeof value !== 'number' || value < 0 || value > 150) {
throw new Error('Invalid age');
}
this._age = value;
},
// Read-only property (getter only)
get birthYear() {
return new Date().getFullYear() - this._age;
}
};
// Using getters and setters
console.log(person.fullName); // "John Doe" (getter called)
person.fullName = 'Jane Smith'; // Setter called
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
console.log(person.age); // 30 (getter)
person.age = 25; // Setter with validation
// person.age = -5; // Error: Invalid age
console.log(person.birthYear); // Computed from age
// person.birthYear = 1990; // TypeError: no setter (read-only)
Example: Getters/setters in classes
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
// Getter for computed property
get area() {
return this._width * this._height;
}
get perimeter() {
return 2 * (this._width + this._height);
}
// Getter/setter with validation
get width() {
return this._width;
}
set width(value) {
if (value <= 0) {
throw new Error('Width must be positive');
}
this._width = value;
}
get height() {
return this._height;
}
set height(value) {
if (value <= 0) {
throw new Error('Height must be positive');
}
this._height = value;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area); // 50 (computed)
console.log(rect.perimeter); // 30 (computed)
rect.width = 20; // Setter with validation
console.log(rect.area); // 100 (auto-updated)
// rect.width = -5; // Error: Width must be positive
// rect.area = 200; // TypeError: no setter (read-only)
Example: Advanced getter/setter patterns
// Lazy initialization with getter
class DataLoader {
constructor(url) {
this.url = url;
this._data = null;
}
get data() {
// Load data on first access, cache for subsequent access
if (this._data === null) {
console.log('Loading data...');
this._data = this.loadData(); // Expensive operation
}
return this._data;
}
loadData() {
// Simulate data loading
return { loaded: true, url: this.url };
}
}
const loader = new DataLoader('/api/data');
console.log(loader.data); // Logs: "Loading data...", returns data
console.log(loader.data); // No log, returns cached data
// Observable pattern with setters
class Observable {
constructor() {
this._observers = [];
this._value = null;
}
get value() {
return this._value;
}
set value(newValue) {
const oldValue = this._value;
this._value = newValue;
this.notify(oldValue, newValue);
}
subscribe(observer) {
this._observers.push(observer);
}
notify(oldValue, newValue) {
this._observers.forEach(fn => fn(newValue, oldValue));
}
}
const obs = new Observable();
obs.subscribe((newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`);
});
obs.value = 10; // Logs: "Changed from null to 10"
obs.value = 20; // Logs: "Changed from 10 to 20"
// Type conversion in setter
const config = {
_port: 8080,
get port() {
return this._port;
},
set port(value) {
// Always store as number
this._port = Number(value);
}
};
config.port = '3000'; // String input
console.log(config.port); // 3000 (number)
console.log(typeof config.port); // 'number'
Best Practice: Use getters for computed properties and derived state. Use setters for
validation, type conversion, and triggering side effects. Follow naming convention: use underscore prefix for
backing fields (e.g., _value). For true privacy in modern JS, use private fields
(#field) instead of convention.
7.7 Object Freezing and Sealing (freeze, seal, preventExtensions)
Method
Add Props
Delete Props
Modify Values
Change Config
Normal Object
✓ Yes
✓ Yes
✓ Yes
✓ Yes
Object.preventExtensions()
✗ No
✓ Yes
✓ Yes
✓ Yes
Object.seal()
✗ No
✗ No
✓ Yes
✗ No
Object.freeze()
✗ No
✗ No
✗ No
✗ No
Method
Effect
Check Method
Use Case
Object.preventExtensions(obj)
Cannot add new properties
Object.isExtensible(obj)
Prevent accidental property additions
Object.seal(obj)
Cannot add/delete, can modify existing
Object.isSealed(obj)
Fixed property set, mutable values
Object.freeze(obj)
Completely immutable (shallow)
Object.isFrozen(obj)
Constants, immutable config
Example: Object.preventExtensions()
const obj = { a: 1, b: 2 };
// Prevent adding new properties
Object.preventExtensions(obj);
// Existing properties work normally
obj.a = 10; // ✓ Works (can modify)
delete obj.b; // ✓ Works (can delete)
console.log(obj); // { a: 10 }
// Cannot add new properties
obj.c = 3; // ✗ Fails silently (strict mode: TypeError)
console.log(obj.c); // undefined
// Check if extensible
console.log(Object.isExtensible(obj)); // false
// Cannot make extensible again (one-way operation)
// No Object.makeExtensible() method
Example: Object.seal()
const user = { name: 'Alice', age: 30 };
// Seal object - fixed property set
Object.seal(user);
// Can modify existing properties
user.name = 'Bob'; // ✓ Works
user.age = 35; // ✓ Works
console.log(user); // { name: 'Bob', age: 35 }
// Cannot add new properties
user.email = 'test@example.com'; // ✗ Fails silently
console.log(user.email); // undefined
// Cannot delete properties
delete user.age; // ✗ Fails silently
console.log(user.age); // 35 (still exists)
// Cannot reconfigure property descriptors
// Object.defineProperty(user, 'name', { enumerable: false }); // ✗ TypeError
// Check if sealed
console.log(Object.isSealed(user)); // true
// Sealed implies non-extensible
console.log(Object.isExtensible(user)); // false
Example: Object.freeze()
const config = {
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
MAX_RETRIES: 3
};
// Freeze object - completely immutable
Object.freeze(config);
// Cannot modify properties
config.TIMEOUT = 10000; // ✗ Fails silently (strict mode: TypeError)
console.log(config.TIMEOUT); // 5000 (unchanged)
// Cannot add properties
config.DEBUG = true; // ✗ Fails silently
console.log(config.DEBUG); // undefined
// Cannot delete properties
delete config.MAX_RETRIES; // ✗ Fails silently
console.log(config.MAX_RETRIES); // 3 (still exists)
// Check if frozen
console.log(Object.isFrozen(config)); // true
// Frozen implies sealed and non-extensible
console.log(Object.isSealed(config)); // true
console.log(Object.isExtensible(config)); // false
// Common use: constants
const CONSTANTS = Object.freeze({
PI: 3.14159,
E: 2.71828,
MAX_INT: 2147483647
});
// CONSTANTS.PI = 3.14; // ✗ Cannot modify
Example: Shallow vs deep freeze
// Object.freeze is SHALLOW - nested objects not frozen
const user = {
name: 'Alice',
settings: {
theme: 'dark',
notifications: true
}
};
Object.freeze(user);
// Top-level frozen
user.name = 'Bob'; // ✗ Fails
console.log(user.name); // 'Alice' (unchanged)
// Nested object NOT frozen
user.settings.theme = 'light'; // ✓ Works!
user.settings.notifications = false; // ✓ Works!
console.log(user.settings.theme); // 'light' (modified)
// Deep freeze implementation
function deepFreeze(obj) {
// Freeze object itself
Object.freeze(obj);
// Recursively freeze all object properties
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = obj[prop];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return obj;
}
const deepUser = {
name: 'Charlie',
settings: { theme: 'dark', notifications: true }
};
deepFreeze(deepUser);
// Now nested is also frozen
deepUser.settings.theme = 'light'; // ✗ Fails
console.log(deepUser.settings.theme); // 'dark' (unchanged)
Example: Practical use cases
// Use case 1: Enum-like constants
const Status = Object.freeze({
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected'
});
// Status.PENDING = 'other'; // ✗ Cannot modify
// Use case 2: Configuration objects
const appConfig = Object.freeze({
version: '1.0.0',
apiEndpoint: '/api/v1',
features: {
auth: true,
analytics: true
}
});
// Use case 3: Prevent accidental mutations in function
function processUser(user) {
// Prevent function from modifying passed object
const immutableUser = Object.freeze({ ...user });
// immutableUser.name = 'Modified'; // ✗ Fails
// Return new object instead
return { ...immutableUser, processed: true };
}
// Use case 4: Sealed object with mutable values
const counters = Object.seal({
clicks: 0,
views: 0,
purchases: 0
});
// Can update counters
counters.clicks++;
counters.views += 10;
// But cannot add new counters
// counters.downloads = 0; // ✗ Fails
// Comparison matrix
const obj1 = { a: 1 };
Object.preventExtensions(obj1);
console.log({
canAdd: !Object.isExtensible(obj1), // true (cannot add)
canDelete: !Object.isSealed(obj1), // true (can delete)
canModify: !Object.isFrozen(obj1) // true (can modify)
});
const obj2 = { a: 1 };
Object.seal(obj2);
console.log({
canAdd: !Object.isExtensible(obj2), // true (cannot add)
canDelete: !Object.isSealed(obj2), // false (cannot delete)
canModify: !Object.isFrozen(obj2) // true (can modify)
});
const obj3 = { a: 1 };
Object.freeze(obj3);
console.log({
canAdd: !Object.isExtensible(obj3), // true (cannot add)
canDelete: !Object.isSealed(obj3), // false (cannot delete)
canModify: !Object.isFrozen(obj3) // false (cannot modify)
});
Important Limitations:
- Shallow only: All three methods (preventExtensions, seal, freeze) only
affect the immediate object, not nested objects
- Irreversible: Cannot undo these operations - once frozen/sealed, always
frozen/sealed
- Silent failures: In non-strict mode, violations fail silently (no error
thrown)
- Performance: Frozen objects may have optimization benefits but are rarely
the bottleneck
Section 7 Summary
- Modern literals: Property/method shorthand, computed properties, spread
syntax, getters/setters for clean object creation
- Property descriptors: Control writable/enumerable/configurable attributes;
defineProperty for fine-grained control
- Object methods: keys/values/entries for iteration; fromEntries for
transformation; hasOwn for safe property checks
- Creation patterns: Object.create for prototypal inheritance; constructors
(legacy) vs classes (modern); factory functions for flexibility
- Cloning: Spread/assign for shallow copy; structuredClone for deep copy;
JSON for simple deep copy (lossy)
- Getters/setters: Computed properties, validation, lazy initialization; use
_prefix convention for backing fields
- Immutability: preventExtensions (no add), seal (no add/delete), freeze (no
changes); all shallow, implement deepFreeze for nested
8. Prototypes and Inheritance System
8.1 Prototype Chain and Property Lookup
Concept
Description
Key Points
Prototype
Internal reference ([[Prototype]]) that links object to another object
Forms inheritance chain, enables property sharing
Prototype Chain
Series of prototype links from object to Object.prototype to null
Property lookup traverses chain until found or reaches null
__proto__
Accessor property to get/set [[Prototype]] (legacy, avoid)
Non-standard but widely supported; use Object.getPrototypeOf() instead
Object.prototype
Top of prototype chain for normal objects; contains toString, hasOwnProperty, etc.
Inherits from null; all objects inherit from it unless explicitly set
Function.prototype
Prototype for all functions; contains call, apply, bind, etc.
Functions inherit from Function.prototype which inherits from Object.prototype
.prototype Property
Property on constructor functions; used as prototype for instances created with new
Only constructor functions have .prototype; instances have [[Prototype]]
Property Lookup Step
Location
Action
1. Own Properties
Object itself
Check if property exists directly on object; return if found
2. Prototype
Object's [[Prototype]]
If not found, check prototype; return if found
3. Prototype Chain
Prototype's prototype, etc.
Continue up chain until property found
4. Object.prototype
Top of chain
Check Object.prototype; return if found
5. null
End of chain
Return undefined if not found anywhere
Example: Understanding the prototype chain
// Every object has internal [[Prototype]] link
const obj = { a: 1 };
// Access prototype using Object.getPrototypeOf()
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// Object.prototype's prototype is null (end of chain)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// Property lookup demonstration
const animal = {
eats: true,
walk() {
console.log('Animal walks');
}
};
const rabbit = {
jumps: true
};
// Set rabbit's prototype to animal
Object.setPrototypeOf(rabbit, animal);
// rabbit can access both own and inherited properties
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
// Property lookup order
console.log(rabbit.toString); // [Function: toString] (from Object.prototype)
// Chain: rabbit -> animal -> Object.prototype -> null
console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Example: Property shadowing and prototype lookup
const parent = {
name: 'Parent',
greet() {
return `Hello from ${this.name}`;
}
};
const child = Object.create(parent);
child.age = 10;
// Access inherited property
console.log(child.name); // 'Parent' (from prototype)
console.log(child.greet()); // "Hello from Parent"
// Own property shadows prototype property
child.name = 'Child';
console.log(child.name); // 'Child' (own property, shadows prototype)
// Check property ownership
console.log(child.hasOwnProperty('name')); // true (own)
console.log(child.hasOwnProperty('greet')); // false (inherited)
console.log('greet' in child); // true (checks own + prototype)
// Delete own property - prototype property revealed
delete child.name;
console.log(child.name); // 'Parent' (prototype property no longer shadowed)
// Writing to inherited property creates own property
const obj = Object.create({ x: 1 });
console.log(obj.x); // 1 (inherited)
obj.x = 2; // Creates own property (doesn't modify prototype)
console.log(obj.x); // 2 (own property)
console.log(Object.getPrototypeOf(obj).x); // 1 (prototype unchanged)
Example: Working with prototypes
// Get prototype of object
const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true
// Set prototype (avoid in production - slow!)
const animal = { type: 'animal' };
const dog = { breed: 'Labrador' };
Object.setPrototypeOf(dog, animal);
console.log(dog.type); // 'animal' (inherited)
// Create object with specific prototype (preferred)
const cat = Object.create(animal);
cat.breed = 'Persian';
console.log(cat.type); // 'animal' (inherited)
// Check if object is in prototype chain
console.log(animal.isPrototypeOf(dog)); // true
console.log(animal.isPrototypeOf(cat)); // true
console.log(Object.prototype.isPrototypeOf(dog)); // true (all objects)
// Create object with null prototype (no inheritance)
const noProto = Object.create(null);
console.log(noProto.toString); // undefined (no Object.prototype)
console.log(Object.getPrototypeOf(noProto)); // null
// Prototype chain visualization
function showChain(obj, label = 'obj') {
console.log(`${label}:`, obj);
const proto = Object.getPrototypeOf(obj);
if (proto) {
showChain(proto, `${label}.__proto__`);
} else {
console.log('End of chain: null');
}
}
const myObj = { x: 1 };
showChain(myObj);
// myObj: { x: 1 }
// myObj.__proto__: Object.prototype
// End of chain: null
Important: Property lookup is read-only up the chain. Writing to a property always
creates/modifies own property (unless setter exists in prototype). Use Object.getPrototypeOf() and
Object.create() instead of deprecated __proto__. Avoid
Object.setPrototypeOf() for performance reasons.
8.2 Constructor Functions and new Operator
Concept
Description
Behavior
Constructor Function
Regular function used with new to create instances
Conventionally capitalized; sets up instance properties
new Operator
Creates instance, sets prototype, binds this, returns object
4-step process: create, link, execute, return
Constructor.prototype
Object that becomes prototype of instances created with new Constructor()
Shared methods placed here for memory efficiency
constructor Property
Property on prototype pointing back to constructor function
Allows instances to identify their constructor
Instance Properties
Properties unique to each instance (set in constructor)
Defined using this.property = value
Prototype Methods
Methods shared across all instances (on Constructor.prototype)
Memory efficient - one copy shared by all instances
Step
new Operator Process
Effect
1. Create
New empty object created
const obj = {}
2. Link Prototype
Object's [[Prototype]] set to Constructor.prototype
Object.setPrototypeOf(obj, Constructor.prototype)
3. Execute Constructor
Constructor called with this bound to new object
Constructor.call(obj, ...args)
4. Return
Return object (or constructor's return if object)
Returns this unless constructor explicitly returns object
Example: Constructor functions and instances
// Constructor function (capitalized by convention)
function Person(name, age) {
// Instance properties (unique per instance)
this.name = name;
this.age = age;
// ✗ Avoid methods here (creates new function per instance)
// this.greet = function() { return `Hi, I'm ${this.name}`; };
}
// Shared methods on prototype (memory efficient)
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
return this.age;
};
// Create instances with 'new'
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
// Each instance has own properties
console.log(alice.name); // 'Alice'
console.log(bob.name); // 'Bob'
// Shared methods from prototype
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.greet()); // "Hello, I'm Bob"
// Methods are shared (same function object)
console.log(alice.greet === bob.greet); // true (memory efficient)
// Verify prototype relationship
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice.constructor === Person); // true
console.log(alice instanceof Person); // true
// Prototype chain: alice -> Person.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
Example: Manual simulation of 'new' operator
// Understanding what 'new' does
function myNew(Constructor, ...args) {
// 1. Create new object
const obj = {};
// 2. Link to prototype
Object.setPrototypeOf(obj, Constructor.prototype);
// Or: obj.__proto__ = Constructor.prototype;
// 3. Execute constructor with 'this' bound to new object
const result = Constructor.apply(obj, args);
// 4. Return object (or constructor's return if it's an object)
return (typeof result === 'object' && result !== null) ? result : obj;
}
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
// Using custom 'new'
const dog1 = myNew(Dog, 'Rex');
console.log(dog1.name); // 'Rex'
console.log(dog1.bark()); // "Rex says woof!"
console.log(dog1 instanceof Dog); // true
// Compare with real 'new'
const dog2 = new Dog('Max');
console.log(dog2.name); // 'Max'
console.log(dog2.bark()); // "Max says woof!"
// Constructor returning object overrides default behavior
function Special() {
this.x = 1;
return { y: 2 }; // Explicit object return
}
const s = new Special();
console.log(s.x); // undefined (returned object used instead)
console.log(s.y); // 2
// Constructor returning primitive doesn't override
function Normal() {
this.x = 1;
return 42; // Primitive ignored
}
const n = new Normal();
console.log(n.x); // 1 (this returned as usual)
Example: Constructor patterns and best practices
// Pattern 1: Instance properties + prototype methods (recommended)
function Car(make, model) {
this.make = make;
this.model = model;
}
Car.prototype.getDescription = function() {
return `${this.make} ${this.model}`;
};
// Pattern 2: Static methods (on constructor itself)
Car.fromString = function(str) {
const [make, model] = str.split(' ');
return new Car(make, model);
};
const car1 = new Car('Toyota', 'Camry');
const car2 = Car.fromString('Honda Civic');
// Pattern 3: Guard against missing 'new'
function SafeConstructor(value) {
// Check if called with 'new'
if (!(this instanceof SafeConstructor)) {
return new SafeConstructor(value);
}
this.value = value;
}
const obj1 = new SafeConstructor(10); // With 'new'
const obj2 = SafeConstructor(20); // Without 'new' - still works
console.log(obj1 instanceof SafeConstructor); // true
console.log(obj2 instanceof SafeConstructor); // true
// Pattern 4: Setting prototype after constructor definition
function Animal(name) {
this.name = name;
}
// Replace entire prototype (need to restore constructor)
Animal.prototype = {
constructor: Animal, // Restore constructor reference
speak() {
return `${this.name} makes a sound`;
},
eat() {
return `${this.name} is eating`;
}
};
const animal = new Animal('Lion');
console.log(animal.speak()); // "Lion makes a sound"
console.log(animal.constructor === Animal); // true (restored)
// ✗ Common mistake: adding methods as instance properties
function Inefficient(name) {
this.name = name;
// ✗ Bad: creates new function for each instance
this.greet = function() {
return `Hi, I'm ${this.name}`;
};
}
const i1 = new Inefficient('A');
const i2 = new Inefficient('B');
console.log(i1.greet === i2.greet); // false (different functions - wasteful)
Common Pitfalls:
- Forgetting 'new': Constructor called as regular function;
this
refers to global object (or undefined in strict mode)
- Methods as instance properties: Creating functions inside constructor
wastes memory; use prototype instead
- Overwriting prototype: Breaks constructor reference; always restore
.constructor property
- Modifying built-in prototypes: Never modify Object.prototype,
Array.prototype, etc. (causes conflicts)
8.3 Object.create and Prototypal Inheritance
Method
Syntax
Creates
Use Case
Object.create(proto)
Object.create(protoObj)
New object with protoObj as prototype
Prototypal inheritance, object delegation
Object.create(proto, descriptors)
Object.create(proto, { props })
Object with prototype + property descriptors
Fine-grained property control
Object.create(null)
Object.create(null)
Object with no prototype (null)
Pure data storage, avoid prototype pollution
Inheritance Pattern
Approach
Pros
Cons
Constructor Pattern
function + new + prototype
Familiar to class-based developers
Verbose, easy to forget 'new'
Object.create Pattern
Object.create(proto)
Simple, pure prototypal, no 'new'
Less familiar, no constructor function
ES6 Class
class syntax
Clean syntax, modern, standard
Syntactic sugar over prototypes
Factory Function
function returning object
Flexible, private state via closure
No shared prototype methods (unless manually added)
Example: Object.create for prototypal inheritance
// Parent object (prototype)
const animal = {
type: 'animal',
eat() {
return `${this.name} is eating`;
},
sleep() {
return `${this.name} is sleeping`;
}
};
// Create child objects inheriting from parent
const dog = Object.create(animal);
dog.name = 'Rex';
dog.breed = 'Labrador';
dog.bark = function() {
return `${this.name} says woof!`;
};
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.breed = 'Persian';
cat.meow = function() {
return `${this.name} says meow!`;
};
// Access own properties
console.log(dog.name); // 'Rex' (own)
console.log(dog.breed); // 'Labrador' (own)
// Access inherited properties/methods
console.log(dog.type); // 'animal' (inherited)
console.log(dog.eat()); // "Rex is eating" (inherited method)
console.log(dog.bark()); // "Rex says woof!" (own method)
// Verify prototype chain
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(cat) === animal); // true
// Shared prototype
console.log(dog.eat === cat.eat); // true (same method from prototype)
// Modify prototype affects all children
animal.move = function() {
return `${this.name} is moving`;
};
console.log(dog.move()); // "Rex is moving" (newly added)
console.log(cat.move()); // "Whiskers is moving"
Example: Object.create with property descriptors
const parent = {
greet() {
return `Hello from ${this.name}`;
}
};
// Create object with prototype and property descriptors
const child = Object.create(parent, {
name: {
value: 'Child',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 10,
writable: true,
enumerable: true,
configurable: true
},
// Accessor property
description: {
get() {
return `${this.name}, age ${this.age}`;
},
enumerable: true,
configurable: true
}
});
console.log(child.name); // 'Child'
console.log(child.age); // 10
console.log(child.greet()); // "Hello from Child" (inherited)
console.log(child.description); // "Child, age 10" (computed)
// Object with null prototype (no inheritance)
const dict = Object.create(null);
dict.key1 = 'value1';
dict.key2 = 'value2';
console.log(dict.toString); // undefined (no Object.prototype)
console.log(dict.hasOwnProperty); // undefined
console.log(Object.getPrototypeOf(dict)); // null
// Useful for pure data storage
console.log('toString' in dict); // false (safe from prototype pollution)
Example: Implementing classical inheritance with Object.create
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name} is eating`;
};
Animal.prototype.sleep = function() {
return `${this.name} is sleeping`;
};
// Child constructor
function Dog(name, breed) {
// Call parent constructor
Animal.call(this, name); // Inherit instance properties
this.breed = breed;
}
// Set up prototype chain (critical step)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Restore constructor reference
// Add child-specific methods
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
// Create instance
const dog = new Dog('Rex', 'Labrador');
console.log(dog.name); // 'Rex' (from Animal)
console.log(dog.breed); // 'Labrador' (from Dog)
console.log(dog.eat()); // "Rex is eating" (inherited from Animal)
console.log(dog.bark()); // "Rex says woof!" (from Dog)
// Verify prototype chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog.constructor === Dog); // true
// Chain: dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
// ✗ Wrong way (don't do this)
// Dog.prototype = Animal.prototype; // Shares prototype (modifications affect both)
// Dog.prototype = new Animal(); // Creates unnecessary instance
// ✓ Correct way (ES6+ alternative)
class AnimalClass {
constructor(name) {
this.name = name;
}
eat() {
return `${this.name} is eating`;
}
}
class DogClass extends AnimalClass {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
return `${this.name} says woof!`;
}
}
const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.eat()); // "Max is eating"
console.log(modernDog.bark()); // "Max says woof!"
Best Practice: For inheritance, use ES6 classes (cleaner syntax). If using constructor
functions, always use Object.create(Parent.prototype) to set up prototype chain, not
new Parent() or direct assignment. Always restore constructor property. Use
Object.create(null) for dictionary objects to avoid prototype pollution.
8.4 Prototype Methods and Property Override
Concept
Description
Behavior
Method Inheritance
Child inherits methods from parent's prototype
Shared method executed with child instance as this
Method Override
Child defines method with same name as parent
Child method shadows parent method in prototype chain
super Pattern
Call parent method from overridden child method
Use Parent.prototype.method.call(this) or ES6 super
Property Shadowing
Own property hides prototype property with same name
Lookup stops at first match (own properties checked first)
Dynamic Dispatch
this binding determined at call time, not definition
Enables polymorphism - same method name, different behavior
Pattern
Implementation
Use Case
Simple Override
Define method on child with same name
Completely replace parent behavior
Extend Parent Method
Call parent method + add child logic
Augment parent behavior
Delegate to Parent
Forward some calls to parent method
Conditional behavior based on state
Mixin Pattern
Copy methods from multiple sources
Multiple inheritance simulation
Example: Method inheritance and override
// Parent with methods
const animal = {
speak() {
return `${this.name} makes a sound`;
},
eat() {
return `${this.name} is eating`;
}
};
// Child inherits and overrides
const dog = Object.create(animal);
dog.name = 'Rex';
// Inherited method (no override)
console.log(dog.eat()); // "Rex is eating" (from animal)
// Override speak method
dog.speak = function() {
return `${this.name} barks loudly`;
};
console.log(dog.speak()); // "Rex barks loudly" (overridden)
// Access parent method explicitly
console.log(animal.speak.call(dog)); // "Rex makes a sound" (parent method)
// Another child with different override
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.speak = function() {
return `${this.name} meows softly`;
};
console.log(cat.speak()); // "Whiskers meows softly" (different override)
// Polymorphism in action
const animals = [dog, cat];
animals.forEach(a => console.log(a.speak()));
// "Rex barks loudly"
// "Whiskers meows softly"
Example: Calling parent methods (super pattern)
// Constructor-based inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.describe = function() {
return `This is ${this.name}`;
};
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Override with extension - call parent method
Dog.prototype.describe = function() {
// Call parent method
const parentDesc = Animal.prototype.describe.call(this);
// Add child-specific info
return `${parentDesc}, a ${this.breed}`;
};
Dog.prototype.speak = function() {
// Completely override (no parent call)
return `${this.name} barks`;
};
const dog = new Dog('Rex', 'Labrador');
console.log(dog.describe()); // "This is Rex, a Labrador" (extended)
console.log(dog.speak()); // "Rex barks" (overridden)
// ES6 class with super keyword (cleaner)
class AnimalClass {
constructor(name) {
this.name = name;
}
describe() {
return `This is ${this.name}`;
}
speak() {
return `${this.name} makes a sound`;
}
}
class DogClass extends AnimalClass {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
describe() {
// Call parent method with super
return `${super.describe()}, a ${this.breed}`;
}
speak() {
// Override without calling parent
return `${this.name} barks`;
}
}
const modernDog = new DogClass('Max', 'Beagle');
console.log(modernDog.describe()); // "This is Max, a Beagle"
console.log(modernDog.speak()); // "Max barks"
Example: Property shadowing and resolution
const parent = {
name: 'Parent',
value: 10,
getValue() {
return this.value;
}
};
const child = Object.create(parent);
console.log(child.name); // 'Parent' (inherited)
console.log(child.value); // 10 (inherited)
// Shadow property with own value
child.name = 'Child';
child.value = 20;
console.log(child.name); // 'Child' (own property shadows parent)
console.log(child.value); // 20 (own)
console.log(parent.name); // 'Parent' (parent unchanged)
// Method uses 'this.value' - gets child's own value
console.log(child.getValue()); // 20 (inherited method, child's value)
// Delete own property - parent property revealed
delete child.name;
console.log(child.name); // 'Parent' (parent property no longer shadowed)
// hasOwnProperty distinguishes own vs inherited
console.log(child.hasOwnProperty('value')); // true (own)
console.log(child.hasOwnProperty('getValue')); // false (inherited)
// Writing to inherited property creates own property
const obj = Object.create({ x: 1, y: 2 });
obj.x = 10; // Creates own property
console.log(obj.x); // 10 (own)
console.log(Object.getPrototypeOf(obj).x); // 1 (prototype unchanged)
// Arrays inherit push, pop, etc. from Array.prototype
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('push')); // false (inherited)
console.log(arr.hasOwnProperty('length')); // true (own property)
console.log(arr.hasOwnProperty(0)); // true (own property)
Example: Mixin pattern for method sharing
// Mixin: reusable method collections
const canEat = {
eat(food) {
return `${this.name} is eating ${food}`;
}
};
const canWalk = {
walk() {
return `${this.name} is walking`;
}
};
const canSwim = {
swim() {
return `${this.name} is swimming`;
}
};
// Mixin helper function
function mixin(target, ...sources) {
Object.assign(target, ...sources);
return target;
}
// Create object with multiple mixins
function Animal(name) {
this.name = name;
}
// Add mixins to prototype
mixin(Animal.prototype, canEat, canWalk);
const dog = new Animal('Rex');
console.log(dog.eat('bone')); // "Rex is eating bone"
console.log(dog.walk()); // "Rex is walking"
// console.log(dog.swim()); // Error: swim not available
// Different mix of capabilities
function Fish(name) {
this.name = name;
}
mixin(Fish.prototype, canEat, canSwim);
const fish = new Fish('Nemo');
console.log(fish.eat('algae')); // "Nemo is eating algae"
console.log(fish.swim()); // "Nemo is swimming"
// console.log(fish.walk()); // Error: walk not available
// ES6 class with mixins
const Flyable = {
fly() {
return `${this.name} is flying`;
}
};
class Bird {
constructor(name) {
this.name = name;
}
}
// Apply mixin to class
Object.assign(Bird.prototype, canEat, Flyable);
const bird = new Bird('Tweety');
console.log(bird.eat('seeds')); // "Tweety is eating seeds"
console.log(bird.fly()); // "Tweety is flying"
Best Practice: Use method override for polymorphism. Call parent methods with
super (ES6 classes) or Parent.prototype.method.call(this) (constructor functions). Use
mixins for horizontal code reuse. Remember: methods use this binding, so inherited methods access
instance properties correctly.
8.5 instanceof and Prototype Testing
Method/Operator
Syntax
Returns
What It Checks
instanceof
obj instanceof Constructor
boolean
Is Constructor.prototype in obj's prototype chain?
isPrototypeOf()
proto.isPrototypeOf(obj)
boolean
Is proto in obj's prototype chain?
Object.getPrototypeOf()
Object.getPrototypeOf(obj)
object or null
Returns obj's direct prototype
hasOwnProperty()
obj.hasOwnProperty(prop)
boolean
Is prop an own property (not inherited)?
Object.hasOwn() ES2022
Object.hasOwn(obj, prop)
boolean
Safer alternative to hasOwnProperty
in operator
'prop' in obj
boolean
Is prop in obj or its prototype chain?
Test
Own Property
Inherited Property
Nonexistent
obj.prop
value
value
undefined
'prop' in obj
true
true
false
obj.hasOwnProperty('prop')
true
false
false
Object.hasOwn(obj, 'prop')
true
false
false
Example: instanceof operator
// Constructor-based type checking
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog = new Dog('Rex', 'Labrador');
// instanceof checks prototype chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true (all objects)
// Arrays
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
// Primitives
console.log('hello' instanceof String); // false (primitive, not object)
console.log(new String('hello') instanceof String); // true (wrapper object)
console.log(42 instanceof Number); // false
// instanceof can be fooled
function Fake() {}
Fake.prototype = dog; // Set prototype to instance
const fake = new Fake();
console.log(fake instanceof Dog); // true! (dog in prototype chain)
// Object.create
const parent = { type: 'parent' };
const child = Object.create(parent);
// No constructor function, instanceof doesn't work well
// console.log(child instanceof ???); // What to check?
Example: isPrototypeOf() method
const animal = {
eats: true
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
const longEaredRabbit = Object.create(rabbit);
longEaredRabbit.earLength = 10;
// Check prototype relationships
console.log(animal.isPrototypeOf(rabbit)); // true
console.log(animal.isPrototypeOf(longEaredRabbit)); // true (anywhere in chain)
console.log(rabbit.isPrototypeOf(longEaredRabbit)); // true
console.log(rabbit.isPrototypeOf(animal)); // false (wrong direction)
// Works with any object, not just constructors
console.log(Object.prototype.isPrototypeOf(rabbit)); // true (all objects)
console.log(Object.prototype.isPrototypeOf({})); // true
// Compare with instanceof (requires constructor)
function Animal() {}
const dog = new Animal();
console.log(dog instanceof Animal); // true
console.log(Animal.prototype.isPrototypeOf(dog)); // true (equivalent)
// null prototype
const noProto = Object.create(null);
console.log(Object.prototype.isPrototypeOf(noProto)); // false
Example: Property ownership testing
const parent = {
inherited: 'from parent',
sharedMethod() {
return 'shared';
}
};
const child = Object.create(parent);
child.own = 'own property';
// 'in' operator - checks own and inherited
console.log('own' in child); // true (own)
console.log('inherited' in child); // true (inherited)
console.log('sharedMethod' in child); // true (inherited)
console.log('nonexistent' in child); // false
// hasOwnProperty - checks only own properties
console.log(child.hasOwnProperty('own')); // true
console.log(child.hasOwnProperty('inherited')); // false (inherited)
console.log(child.hasOwnProperty('sharedMethod')); // false
// Object.hasOwn (ES2022) - safer alternative
console.log(Object.hasOwn(child, 'own')); // true
console.log(Object.hasOwn(child, 'inherited')); // false
// Why Object.hasOwn is safer
const obj = Object.create(null); // No prototype
obj.prop = 'value';
// obj.hasOwnProperty('prop'); // Error: hasOwnProperty not available
console.log(Object.hasOwn(obj, 'prop')); // true (works!)
// Distinguish own vs inherited in iteration
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(`Own: ${key}`);
} else {
console.log(`Inherited: ${key}`);
}
}
// Own: own
// Inherited: inherited
// Inherited: sharedMethod
// Object.keys() only returns own enumerable properties
console.log(Object.keys(child)); // ['own']
// Get all properties including inherited (manual)
function getAllProperties(obj) {
const props = new Set();
let current = obj;
while (current) {
Object.getOwnPropertyNames(current).forEach(p => props.add(p));
current = Object.getPrototypeOf(current);
}
return Array.from(props);
}
console.log(getAllProperties(child));
// Includes: own, inherited, sharedMethod, toString, hasOwnProperty, etc.
Example: Type checking best practices
// Checking built-in types
function checkType(value) {
// Arrays - use Array.isArray (most reliable)
if (Array.isArray(value)) {
return 'array';
}
// null (special case - typeof null === 'object')
if (value === null) {
return 'null';
}
// Primitives - use typeof
if (typeof value !== 'object') {
return typeof value; // 'string', 'number', 'boolean', etc.
}
// Objects - check constructor
if (value instanceof Date) return 'date';
if (value instanceof RegExp) return 'regexp';
if (value instanceof Map) return 'map';
if (value instanceof Set) return 'set';
return 'object';
}
console.log(checkType([1, 2, 3])); // 'array'
console.log(checkType('hello')); // 'string'
console.log(checkType(42)); // 'number'
console.log(checkType(null)); // 'null'
console.log(checkType(new Date())); // 'date'
console.log(checkType({ x: 1 })); // 'object'
// Duck typing - check for expected methods
function isIterable(obj) {
return obj != null && typeof obj[Symbol.iterator] === 'function';
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable('hello')); // true
console.log(isIterable({ x: 1 })); // false
console.log(isIterable(new Map())); // true
// Custom type guard (TypeScript-like pattern in JS)
function isDog(animal) {
return animal
&& typeof animal.bark === 'function'
&& typeof animal.breed === 'string';
}
const dog = { name: 'Rex', breed: 'Lab', bark() {} };
const cat = { name: 'Fluffy', meow() {} };
console.log(isDog(dog)); // true
console.log(isDog(cat)); // false
// Symbol.toStringTag for custom types
class CustomType {
get [Symbol.toStringTag]() {
return 'CustomType';
}
}
const custom = new CustomType();
console.log(Object.prototype.toString.call(custom)); // '[object CustomType]'
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(new Date())); // '[object Date]'
Common Pitfalls:
- instanceof with primitives: Returns false for primitives like
'string' even though they can use String methods
- instanceof across frames: Different window/frame has different built-in
constructors; use Array.isArray() for arrays
- hasOwnProperty safety: Objects with null prototype don't have
hasOwnProperty; use Object.hasOwn() instead
- typeof null: Returns 'object' (JavaScript quirk); check
=== null explicitly
8.6 Object Delegation Patterns
Pattern
Description
Key Concept
Behavior Delegation (OLOO)
Objects delegate to other objects (not classes)
Objects Linked to Other Objects - pure prototypal
Classical Inheritance
Parent-child hierarchy with constructors
Constructor functions + prototype chain
Concatenative Inheritance
Mix properties from multiple sources
Object.assign() / spread for composition
Functional Inheritance
Factory functions with closures for privacy
Functions return objects with private state
Aspect
Classical (Constructor)
Delegation (OLOO)
Syntax
new Constructor()
Object.create(proto)
Focus
Parent-child classes
Peer objects delegating to each other
Initialization
Constructor function
Init method on prototype
Complexity
More verbose, mimics classes
Simpler, embraces prototypes
Mental Model
"This IS-A that" (inheritance)
"This HAS-A reference to that" (delegation)
Example: OLOO pattern (Objects Linked to Other Objects)
// Behavior delegation style (OLOO)
const Vehicle = {
init(make, model) {
this.make = make;
this.model = model;
return this;
},
describe() {
return `${this.make} ${this.model}`;
}
};
const Car = Object.create(Vehicle);
Car.init = function(make, model, doors) {
// Delegate to Vehicle.init
Vehicle.init.call(this, make, model);
this.doors = doors;
return this;
};
Car.drive = function() {
return `Driving ${this.describe()}`;
};
// Create instances
const myCar = Object.create(Car).init('Toyota', 'Camry', 4);
const yourCar = Object.create(Car).init('Honda', 'Civic', 4);
console.log(myCar.describe()); // "Toyota Camry"
console.log(myCar.drive()); // "Driving Toyota Camry"
console.log(yourCar.drive()); // "Driving Honda Civic"
// No constructors, no 'new', just objects delegating
console.log(Object.getPrototypeOf(myCar) === Car); // true
console.log(Object.getPrototypeOf(Car) === Vehicle); // true
// Compare with constructor pattern
function VehicleConstructor(make, model) {
this.make = make;
this.model = model;
}
VehicleConstructor.prototype.describe = function() {
return `${this.make} ${this.model}`;
};
function CarConstructor(make, model, doors) {
VehicleConstructor.call(this, make, model);
this.doors = doors;
}
CarConstructor.prototype = Object.create(VehicleConstructor.prototype);
CarConstructor.prototype.constructor = CarConstructor;
CarConstructor.prototype.drive = function() {
return `Driving ${this.describe()}`;
};
const constructorCar = new CarConstructor('Ford', 'Focus', 4);
console.log(constructorCar.drive()); // Same result, more complex setup
Example: Concatenative inheritance (composition)
// Reusable behavior objects
const canEat = {
eat(food) {
return `${this.name} eats ${food}`;
}
};
const canWalk = {
walk() {
return `${this.name} walks`;
}
};
const canSwim = {
swim() {
return `${this.name} swims`;
}
};
// Compose objects by combining behaviors
function createDog(name) {
return {
name,
...canEat,
...canWalk,
bark() {
return `${this.name} barks`;
}
};
}
function createFish(name) {
return {
name,
...canEat,
...canSwim
};
}
function createDuck(name) {
return {
name,
...canEat,
...canWalk,
...canSwim,
quack() {
return `${this.name} quacks`;
}
};
}
const dog = createDog('Rex');
console.log(dog.eat('bone')); // "Rex eats bone"
console.log(dog.walk()); // "Rex walks"
console.log(dog.bark()); // "Rex barks"
// console.log(dog.swim()); // undefined (no swim capability)
const duck = createDuck('Donald');
console.log(duck.eat('bread')); // "Donald eats bread"
console.log(duck.walk()); // "Donald walks"
console.log(duck.swim()); // "Donald swims"
console.log(duck.quack()); // "Donald quacks"
// Advanced: functional mixin
const withLogging = (obj) => {
const originalMethods = {};
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'function') {
originalMethods[key] = obj[key];
obj[key] = function(...args) {
console.log(`Calling ${key} with`, args);
return originalMethods[key].apply(this, args);
};
}
});
return obj;
};
const loggedDog = withLogging(createDog('Max'));
loggedDog.eat('treat'); // Logs: "Calling eat with ['treat']"
// Returns: "Max eats treat"
Example: Functional inheritance with closures
// Factory function with private state
function createBankAccount(initialBalance) {
// Private variables (closure)
let balance = initialBalance;
let transactionHistory = [];
// Private function
function recordTransaction(type, amount) {
transactionHistory.push({
type,
amount,
timestamp: Date.now(),
balance
});
}
// Public interface
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
recordTransaction('deposit', amount);
}
return balance;
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
recordTransaction('withdraw', amount);
}
return balance;
},
getBalance() {
return balance;
},
getHistory() {
// Return copy to prevent external modification
return [...transactionHistory];
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined (private!)
console.log(account.getHistory().length); // 2 transactions
// Inheritance with functional pattern
function createSavingsAccount(initialBalance, interestRate) {
// Get base account
const account = createBankAccount(initialBalance);
// Add new functionality
account.applyInterest = function() {
const interest = account.getBalance() * interestRate;
account.deposit(interest);
return account.getBalance();
};
return account;
}
const savings = createSavingsAccount(1000, 0.05);
savings.deposit(500); // 1500
savings.applyInterest(); // 1575 (5% interest)
console.log(savings.getBalance()); // 1575
Example: Practical delegation patterns
// Plugin system using delegation
const BaseApp = {
init(name) {
this.name = name;
this.plugins = [];
return this;
},
use(plugin) {
this.plugins.push(plugin);
if (plugin.install) {
plugin.install(this);
}
return this;
},
run() {
console.log(`${this.name} is running`);
this.plugins.forEach(p => p.onRun && p.onRun(this));
}
};
// Specific app type
const WebApp = Object.create(BaseApp);
WebApp.init = function(name, port) {
BaseApp.init.call(this, name);
this.port = port;
return this;
};
WebApp.start = function() {
console.log(`Server started on port ${this.port}`);
this.run();
};
// Plugins
const LoggerPlugin = {
install(app) {
console.log(`Logger installed in ${app.name}`);
},
onRun(app) {
console.log(`[LOG] ${app.name} executed`);
}
};
const CachePlugin = {
install(app) {
app.cache = new Map();
console.log(`Cache installed in ${app.name}`);
},
onRun(app) {
console.log(`[CACHE] Size: ${app.cache.size}`);
}
};
// Create app with plugins
const app = Object.create(WebApp)
.init('MyApp', 3000)
.use(LoggerPlugin)
.use(CachePlugin);
app.start();
// Logger installed in MyApp
// Cache installed in MyApp
// Server started on port 3000
// MyApp is running
// [LOG] MyApp executed
// [CACHE] Size: 0
Section 8 Summary
- Prototype chain: Property lookup traverses [[Prototype]] links from object
to Object.prototype to null; use getPrototypeOf(), not __proto__
- Constructor pattern: Function with
new creates instances; 4
steps: create, link prototype, execute, return; methods on prototype for efficiency
- Object.create: Direct prototypal inheritance without constructors; clean
delegation; use for pure prototypal patterns
- Method override: Child methods shadow parent; call parent with
super or Parent.prototype.method.call(this); enables polymorphism
- Type testing: instanceof checks constructor chain; isPrototypeOf() checks
object chain; Object.hasOwn() for property ownership (safer than hasOwnProperty)
- Delegation patterns: OLOO for pure delegation; concatenative for
composition; functional for privacy; choose based on needs
9. Arrays and Array Methods Reference
9.1 Array Creation and Initialization Patterns
Pattern
Syntax
Description
Example
Array Literal
[el1, el2, ...]
Most common creation method; supports mixed types and sparse arrays
const arr = [1, 2, 3];
Array Constructor
new Array(length)
Creates array with specified length (holes); avoid for single numeric argument
new Array(3) // [empty × 3]
Array.of()
Array.of(...items)
Creates array from arguments; fixes Array() single-number issue
Array.of(3) // [3]
Array.from()
Array.from(iterable, mapFn)
Creates array from iterable/array-like; optional mapper function
Array.from('abc') // ['a','b','c']
Spread Syntax
[...iterable]
Shallow copy or convert iterable to array; modern and concise
[...set] // array from Set
Array.fill()
Array(n).fill(value)
Create pre-filled array; value is shared reference for objects
Array(3).fill(0) // [0,0,0]
Array.keys()
Array.from(Array(n).keys())
Generate sequential number array from 0 to n-1
[...Array(3).keys()] // [0,1,2]
Example: Various array creation patterns
// Literal - most common
const fruits = ['apple', 'banana', 'orange'];
// Array.of - safe with single number
const single = Array.of(5); // [5] not [empty × 5]
// Array.from with mapper
const squares = Array.from({length: 5}, (_, i) => i ** 2);
// [0, 1, 4, 9, 16]
// From string
const chars = Array.from('hello'); // ['h','e','l','l','o']
// From Set
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
// Pre-fill with value
const zeros = Array(10).fill(0);
// Sequential numbers
const range = [...Array(5).keys()]; // [0, 1, 2, 3, 4]
const rangeFrom1 = Array.from({length: 5}, (_, i) => i + 1);
// [1, 2, 3, 4, 5]
// 2D array (careful with fill!)
const matrix = Array(3).fill(null).map(() => Array(3).fill(0));
Note: Avoid new Array(n).fill([]) for 2D arrays as all rows share same reference.
Use .map(() => []) instead.
9.2 Array Mutating Methods (push, pop, splice, sort)
Method
Syntax
Description
Returns
push()
arr.push(el1, ...)
Adds elements to end; modifies original array; O(1) time
New length
pop()
arr.pop()
Removes and returns last element; O(1) time
Removed element
unshift()
arr.unshift(el1, ...)
Adds elements to beginning; shifts indices; O(n) time
New length
shift()
arr.shift()
Removes and returns first element; shifts indices; O(n) time
Removed element
splice()
arr.splice(start, deleteCount, ...items)
Add/remove elements at any position; powerful but complex
Array of deleted elements
sort()
arr.sort(compareFn)
Sorts in-place; default: string comparison; provide compareFn for numbers
Sorted array (same reference)
reverse()
arr.reverse()
Reverses array in-place; O(n) time
Reversed array (same reference)
fill()
arr.fill(value, start, end)
Fills with static value; optional range; O(n) time
Modified array
copyWithin()
arr.copyWithin(target, start, end)
Shallow copies part of array to another location in same array
Modified array
Example: Mutating array methods
const arr = [1, 2, 3];
// Stack operations (end)
arr.push(4, 5); // [1,2,3,4,5] returns 5
arr.pop(); // [1,2,3,4] returns 5
// Queue operations (beginning)
arr.unshift(0); // [0,1,2,3,4] returns 5
arr.shift(); // [1,2,3,4] returns 0
// splice - remove
const items = [1, 2, 3, 4, 5];
items.splice(2, 2); // [1,2,5] returns [3,4]
// splice - insert
items.splice(1, 0, 'a', 'b'); // [1,'a','b',2,5]
// splice - replace
items.splice(0, 2, 'x'); // ['x','b',2,5]
// sort - numbers (must provide compareFn!)
const nums = [10, 2, 5, 1];
nums.sort(); // [1, 10, 2, 5] ❌ string sort!
nums.sort((a, b) => a - b); // [1, 2, 5, 10] ✓
// sort - objects
const users = [{age: 30}, {age: 20}, {age: 25}];
users.sort((a, b) => a.age - b.age);
// reverse
[1, 2, 3].reverse(); // [3, 2, 1]
// fill
Array(5).fill(0); // [0,0,0,0,0]
[1,2,3,4,5].fill(0, 2, 4); // [1,2,0,0,5]
// copyWithin
[1,2,3,4,5].copyWithin(0, 3); // [4,5,3,4,5]
Warning: Default sort() converts to strings! Always provide compare function for
numbers: arr.sort((a,b) => a - b)
9.3 Array Non-mutating Methods (slice, concat, join)
Method
Syntax
Description
Returns
slice()
arr.slice(start, end)
Shallow copy of portion; negative indices from end; doesn't modify original
New array
concat()
arr.concat(arr2, ...)
Merges arrays/values; shallow copy; doesn't modify originals
New array
join()
arr.join(separator)
Converts to string with separator; default comma; doesn't modify
String
toString()
arr.toString()
Converts to comma-separated string; same as join(',')
String
toLocaleString()
arr.toLocaleString()
Locale-aware string conversion; respects number/date formatting
String
flat() ES2019
arr.flat(depth)
Flattens nested arrays; default depth 1; Infinity for full flatten
New flattened array
flatMap() ES2019
arr.flatMap(fn)
Maps then flattens (depth 1); more efficient than map().flat()
New array
with() ES2023
arr.with(index, value)
Returns new array with element replaced; immutable alternative to arr[i]=val
New array
toReversed() ES2023
arr.toReversed()
Non-mutating reverse; returns new reversed array
New reversed array
toSorted() ES2023
arr.toSorted(compareFn)
Non-mutating sort; returns new sorted array
New sorted array
toSpliced() ES2023
arr.toSpliced(start, deleteCount, ...items)
Non-mutating splice; returns new array with changes
New array
Example: Non-mutating array methods
const arr = [1, 2, 3, 4, 5];
// slice - extract portion
arr.slice(1, 3); // [2, 3]
arr.slice(-2); // [4, 5] (last 2)
arr.slice(); // [1,2,3,4,5] (shallow copy)
// concat - merge
[1, 2].concat([3, 4]); // [1, 2, 3, 4]
[1].concat(2, [3, 4], 5); // [1, 2, 3, 4, 5]
// join - to string
['a', 'b', 'c'].join(); // "a,b,c"
['a', 'b', 'c'].join(''); // "abc"
['a', 'b', 'c'].join(' - '); // "a - b - c"
// flat - nested arrays
[1, [2, 3], [4, [5]]].flat(); // [1, 2, 3, 4, [5]]
[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]
// flatMap - map then flatten
['hello', 'world'].flatMap(s => s.split(''));
// ['h','e','l','l','o','w','o','r','l','d']
[1, 2, 3].flatMap(x => [x, x * 2]);
// [1, 2, 2, 4, 3, 6]
// ES2023 immutable methods
const original = [3, 1, 2];
original.toSorted(); // [1, 2, 3]
console.log(original); // [3, 1, 2] unchanged
original.toReversed(); // [2, 1, 3]
original.with(1, 99); // [3, 99, 2]
original.toSpliced(1, 1, 'a', 'b'); // [3, 'a', 'b', 2]
Note: Use spread [...arr] or slice() for shallow copy. ES2023 adds
toSorted(), toReversed(), toSpliced(), with() for immutable
operations.
9.4 Array Iteration Methods (forEach, map, filter, reduce)
Method
Syntax
Description
Returns
forEach()
arr.forEach((el, i, arr) => {})
Execute function for each element; no return value; can't break/continue
undefined
map()
arr.map((el, i, arr) => {})
Transform each element; returns new array with results
New array
filter()
arr.filter((el, i, arr) => {})
Keep elements that pass test; returns new array with matches
New array
reduce()
arr.reduce((acc, el, i, arr) => {}, init)
Reduce to single value; accumulator pattern; init value recommended
Any value
reduceRight()
arr.reduceRight((acc, el) => {}, init)
Like reduce but right-to-left; useful for right-associative operations
Any value
flatMap()
arr.flatMap((el, i, arr) => {})
Map then flatten depth 1; efficient for map + flat combination
New flattened array
entries()
arr.entries()
Returns iterator of [index, value] pairs
Iterator
keys()
arr.keys()
Returns iterator of indices
Iterator
values()
arr.values()
Returns iterator of values
Iterator
Example: Array iteration methods
const nums = [1, 2, 3, 4, 5];
// forEach - side effects only
nums.forEach((n, i) => console.log(`${i}: ${n}`));
// map - transform
const doubled = nums.map(n => n * 2); // [2,4,6,8,10]
const objects = nums.map(n => ({value: n}));
// filter - select
const evens = nums.filter(n => n % 2 === 0); // [2,4]
const large = nums.filter(n => n > 3); // [4,5]
// reduce - accumulate
const sum = nums.reduce((acc, n) => acc + n, 0); // 15
const product = nums.reduce((acc, n) => acc * n, 1); // 120
// reduce - group by
const items = ['a', 'b', 'c', 'a', 'b'];
const counts = items.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {}); // {a: 2, b: 2, c: 1}
// reduce - flatten
[[1, 2], [3, 4], [5]].reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5] (use flat() instead!)
// Method chaining
nums
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0); // 12
// flatMap - map and flatten
['hello', 'world'].flatMap(w => w.split(''));
// ['h','e','l','l','o','w','o','r','l','d']
// Iterators
for (const [i, val] of nums.entries()) {
console.log(i, val); // 0 1, 1 2, ...
}
for (const i of nums.keys()) {
console.log(i); // 0, 1, 2, 3, 4
}
Note: Always provide initial value to reduce() to avoid errors on empty arrays.
Use for...of if you need to break/continue; forEach() can't be stopped early.
9.5 Array Search Methods (find, includes, indexOf)
Method
Syntax
Description
Returns
find()
arr.find((el, i, arr) => {})
Returns first element that passes test; stops on first match
Element or undefined
findLast() ES2023
arr.findLast((el, i, arr) => {})
Like find() but searches right-to-left; returns last match
Element or undefined
findIndex()
arr.findIndex((el, i, arr) => {})
Returns index of first element that passes test
Index or -1
findLastIndex() ES2023
arr.findLastIndex((el, i, arr) => {})
Like findIndex() but searches right-to-left
Index or -1
includes()
arr.includes(value, fromIndex)
Checks if array contains value; uses SameValueZero (handles NaN)
Boolean
indexOf()
arr.indexOf(value, fromIndex)
Returns first index of value; uses strict equality (===); -1 if not found
Index or -1
lastIndexOf()
arr.lastIndexOf(value, fromIndex)
Returns last index of value; searches backwards
Index or -1
at() ES2022
arr.at(index)
Returns element at index; negative indices from end; undefined if out of bounds
Element or undefined
Example: Array search methods
const users = [
{id: 1, name: 'Alice', age: 25},
{id: 2, name: 'Bob', age: 30},
{id: 3, name: 'Charlie', age: 25}
];
// find - first match
const bob = users.find(u => u.name === 'Bob');
// {id: 2, name: 'Bob', age: 30}
const young = users.find(u => u.age < 30);
// {id: 1, name: 'Alice', age: 25}
// findLast - last match
const lastYoung = users.findLast(u => u.age === 25);
// {id: 3, name: 'Charlie', age: 25}
// findIndex
const bobIndex = users.findIndex(u => u.name === 'Bob'); // 1
const notFound = users.findIndex(u => u.name === 'Dave'); // -1
// includes - value check
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true ✓ (indexOf fails here)
// indexOf - strict equality
['a', 'b', 'c'].indexOf('b'); // 1
['a', 'b', 'c'].indexOf('d'); // -1
[1, 2, 3, 2].indexOf(2); // 1 (first occurrence)
// lastIndexOf - search backwards
[1, 2, 3, 2, 1].lastIndexOf(2); // 3
[1, 2, 3].lastIndexOf(4); // -1
// at - negative indexing
const arr = ['a', 'b', 'c', 'd'];
arr.at(0); // 'a'
arr.at(-1); // 'd' (last)
arr.at(-2); // 'c' (second to last)
arr.at(10); // undefined
// Comparison: includes vs indexOf
[1, 2, NaN].includes(NaN); // true
[1, 2, NaN].indexOf(NaN); // -1 ❌
// Check existence
if (users.find(u => u.id === 2)) {
// Found
}
// Get index and value
const index = users.findIndex(u => u.age === 25);
if (index !== -1) {
const user = users[index];
}
Note: Use includes() for existence check (handles NaN); find() for
objects with conditions; at() for negative indexing (cleaner than
arr[arr.length - 1]).
9.6 Array Testing Methods (every, some)
Method
Syntax
Description
Returns
every()
arr.every((el, i, arr) => {})
Tests if ALL elements pass test; short-circuits on first false
Boolean
some()
arr.some((el, i, arr) => {})
Tests if ANY element passes test; short-circuits on first true
Boolean
Example: Array testing methods
const nums = [2, 4, 6, 8, 10];
const mixed = [1, 2, 3, 4, 5];
// every - all must pass
nums.every(n => n % 2 === 0); // true (all even)
mixed.every(n => n % 2 === 0); // false
nums.every(n => n > 0); // true (all positive)
nums.every(n => n > 5); // false
// some - at least one must pass
mixed.some(n => n % 2 === 0); // true (has even)
mixed.some(n => n > 10); // false
[1, 3, 5].some(n => n % 2 === 0); // false (no even)
// Validation use cases
const users = [
{name: 'Alice', age: 25},
{name: 'Bob', age: 30},
{name: 'Charlie', age: 35}
];
// All adults?
const allAdults = users.every(u => u.age >= 18); // true
// Any seniors?
const hasSeniors = users.some(u => u.age >= 65); // false
// All have names?
const allNamed = users.every(u => u.name && u.name.length > 0);
// Any in 30s?
const hasThirties = users.some(u => u.age >= 30 && u.age < 40);
// Empty array edge cases
[].every(n => n > 100); // true (vacuous truth)
[].some(n => n > 100); // false
// Short-circuit behavior
const expensive = [1, 2, 3, 4, 5];
expensive.every(n => {
console.log('checking', n);
return n < 3;
}); // logs: checking 1, checking 2, checking 3
// stops at 3 (first failure)
expensive.some(n => {
console.log('checking', n);
return n > 3;
}); // logs: checking 1, 2, 3, 4
// stops at 4 (first success)
// Combine with other methods
const valid = users
.filter(u => u.age > 25)
.every(u => u.name.length > 0);
Note: every() returns true for empty arrays (vacuous truth); some()
returns false. Both short-circuit, stopping iteration early for performance.
9.7 Array Destructuring and Spread Operations
Pattern
Syntax
Description
Example
Basic Destructuring
const [a, b] = arr
Extract elements into variables by position
const [x, y] = [1, 2]
Skip Elements
const [a, , c] = arr
Omit elements with empty comma slots
const [, , z] = [1, 2, 3]
Rest Pattern
const [a, ...rest] = arr
Collect remaining elements; must be last
const [h, ...t] = [1,2,3]
Default Values
const [a = 0] = arr
Provide fallback if undefined; not for null
const [x = 10] = []
Nested Destructuring
const [[a], [b]] = arr
Destructure nested arrays recursively
const [[x]] = [[1]]
Swap Variables
[a, b] = [b, a]
Elegant variable swap without temp
[x, y] = [y, x]
Spread in Array
[...arr]
Shallow copy or expand iterable
[...arr1, ...arr2]
Spread in Call
fn(...arr)
Pass array elements as separate arguments
Math.max(...nums)
Rest in Function
fn(...args)
Collect function arguments into array
function sum(...n)
Example: Destructuring and spread patterns
// Basic destructuring
const [a, b, c] = [1, 2, 3];
// a=1, b=2, c=3
// Skip elements
const [first, , third] = [1, 2, 3];
// first=1, third=3
// Rest pattern
const [head, ...tail] = [1, 2, 3, 4];
// head=1, tail=[2,3,4]
const [x, y, ...rest] = [1];
// x=1, y=undefined, rest=[]
// Default values
const [p = 0, q = 0] = [1];
// p=1, q=0
const [m = 'default'] = [undefined];
// m='default' (undefined triggers default)
const [n = 'default'] = [null];
// n=null (null doesn't trigger default!)
// Nested destructuring
const [[a1, a2], [b1, b2]] = [[1, 2], [3, 4]];
// a1=1, a2=2, b1=3, b2=4
// Swap variables
let i = 1, j = 2;
[i, j] = [j, i]; // i=2, j=1
// Function parameters
function sum([a, b]) {
return a + b;
}
sum([3, 4]); // 7
function coords({x, y, z = 0}) {
return [x, y, z];
}
// Spread - shallow copy
const original = [1, 2, 3];
const copy = [...original]; // [1, 2, 3]
// Spread - merge arrays
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2]; // [1,2,3,4]
const combined = [0, ...arr1, 2.5, ...arr2, 5];
// [0, 1, 2, 2.5, 3, 4, 5]
// Spread - function arguments
const nums = [5, 3, 8, 1];
Math.max(...nums); // 8 (instead of apply)
// Rest parameters
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
function first(a, b, ...rest) {
console.log(rest); // array of remaining args
}
// Convert iterable
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]
const str = 'hello';
const chars = [...str]; // ['h','e','l','l','o']
// Shallow copy warning
const nested = [[1, 2], [3, 4]];
const shallowCopy = [...nested];
shallowCopy[0][0] = 99;
console.log(nested[0][0]); // 99 ❌ (shared reference!)
Warning: Spread creates shallow copy. Nested objects/arrays
share references. Use structuredClone() for deep copy.
9.8 Typed Arrays and Buffer Management
Type
Bytes/Element
Range
Description
Int8Array
1
-128 to 127
Signed 8-bit integer
Uint8Array
1
0 to 255
Unsigned 8-bit integer
Uint8ClampedArray
1
0 to 255 (clamped)
Clamped unsigned 8-bit (for Canvas)
Int16Array
2
-32,768 to 32,767
Signed 16-bit integer
Uint16Array
2
0 to 65,535
Unsigned 16-bit integer
Int32Array
4
-2³¹ to 2³¹-1
Signed 32-bit integer
Uint32Array
4
0 to 2³²-1
Unsigned 32-bit integer
Float32Array
4
±1.2×10⁻³⁸ to ±3.4×10³⁸
32-bit floating point
Float64Array
8
±5.0×10⁻³²⁴ to ±1.8×10³⁰⁸
64-bit floating point
BigInt64Array
8
-2⁶³ to 2⁶³-1
Signed 64-bit BigInt
BigUint64Array
8
0 to 2⁶⁴-1
Unsigned 64-bit BigInt
Buffer API
Syntax
Description
Use Case
ArrayBuffer
new ArrayBuffer(byteLength)
Fixed-length raw binary data buffer; not directly accessible
Binary data storage
DataView
new DataView(buffer, offset, length)
Low-level interface to read/write multiple types in buffer
Mixed-type binary data
SharedArrayBuffer
new SharedArrayBuffer(length)
Shared memory buffer for Web Workers; requires COOP/COEP headers
Multi-threaded data sharing
Example: Typed arrays and buffer operations
// Create typed arrays
const uint8 = new Uint8Array(4); // [0, 0, 0, 0]
const int32 = new Int32Array([1, 2, 3]); // [1, 2, 3]
// From ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 bytes
const view32 = new Int32Array(buffer); // 4 elements (16/4)
const view8 = new Uint8Array(buffer); // 16 elements
// Shared buffer - multiple views
view32[0] = 0x12345678;
console.log(view8[0]); // 0x78 (little-endian)
// Array-like operations (limited)
const arr = new Uint8Array([1, 2, 3, 4]);
arr.length; // 4
arr[0]; // 1
arr.slice(1, 3); // Uint8Array[2, 3]
arr.map(x => x * 2); // Uint8Array[2, 4, 6, 8]
// No push/pop/splice - fixed length!
// arr.push(5); ❌ TypeError
// Overflow behavior
const u8 = new Uint8Array(1);
u8[0] = 256; // wraps to 0
u8[0] = -1; // wraps to 255
const clamped = new Uint8ClampedArray(1);
clamped[0] = 256; // clamps to 255
clamped[0] = -1; // clamps to 0
// DataView - mixed types
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setInt8(0, 127); // byte 0: 127
view.setInt16(1, 1000); // bytes 1-2: 1000
view.setFloat32(4, 3.14); // bytes 4-7: 3.14
view.getInt8(0); // 127
view.getInt16(1); // 1000
view.getFloat32(4); // 3.14...
// Endianness control
view.setInt32(0, 0x12345678, true); // little-endian
view.setInt32(0, 0x12345678, false); // big-endian
// Convert regular array
const regular = [1, 2, 3, 4, 5];
const typed = new Int32Array(regular);
// Convert typed to regular
const back = Array.from(typed);
// or [...typed]
// Use cases
// 1. Binary file I/O
fetch('data.bin')
.then(r => r.arrayBuffer())
.then(buf => new Uint8Array(buf));
// 2. WebGL graphics
const vertices = new Float32Array([
-1.0, -1.0,
1.0, -1.0,
0.0, 1.0
]);
// 3. Image processing
const imageData = ctx.getImageData(0, 0, w, h);
const pixels = imageData.data; // Uint8ClampedArray
// RGBA: [r, g, b, a, r, g, b, a, ...]
// 4. Network protocols
const header = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
// SharedArrayBuffer (requires headers)
const shared = new SharedArrayBuffer(1024);
const worker1View = new Int32Array(shared);
const worker2View = new Int32Array(shared);
// Both workers see same memory
Note: Typed arrays are fixed-length, array-like objects for binary data. Use for
performance-critical operations (WebGL, image processing, binary I/O). Regular arrays support more methods and
dynamic sizing.
Section 9 Summary
- Creation: Use literals
[] for general use;
Array.from() for iterables with mapper; Array.of() to avoid single-number trap;
spread for copying
- Mutating:
push/pop (end O(1)), shift/unshift
(start O(n)), splice (any position); sort needs compare function for numbers;
these modify original
- Non-mutating:
slice (copy), concat (merge),
flat/flatMap (flatten); ES2023 adds toSorted/toReversed/toSpliced/with for
immutability
- Iteration:
map (transform), filter (select),
reduce (accumulate), forEach (side effects); always provide initial value to
reduce
- Search:
find/findIndex (with predicate), includes
(existence, handles NaN), indexOf (position); at() for negative indexing
- Testing:
every (all pass), some (any pass); both
short-circuit; every([]) === true, some([]) === false
- Destructuring:
[a, b] = arr, rest [...rest], skip
with ,, defaults; spread ...arr for copy/merge (shallow!)
- Typed arrays: Fixed-length, type-specific, backed by ArrayBuffer; use for
binary data, WebGL, performance; limited methods vs regular arrays
10. Strings and Template Literals
10.1 String Creation and String Literals
Type
Syntax
Description
Example
Single Quotes
'string'
Basic string literal; escape ' as \'
'Hello world'
Double Quotes
"string"
Identical to single quotes; escape " as \"
"Hello world"
Template Literal
`string`
Backticks; supports interpolation, multiline, and tag functions
`Hello ${name}`
String Constructor
String(value)
Convert to string; returns primitive (without new)
String(123) // "123"
String Object
new String(value)
Creates String object (wrapper); avoid - use primitives
new String("hi")
Escape Sequences
\n \t \\ \' \"
Special characters: newline, tab, backslash, quotes
'Line 1\nLine 2'
Unicode Escape
\uXXXX
4-digit hex Unicode code point (BMP only)
'\u0041' // "A"
Unicode Code Point
\u{X...}
1-6 hex digits; supports full Unicode range (including astral)
'\u{1F600}' // "😀"
Hex Escape
\xXX
2-digit hex byte value (0x00-0xFF)
'\x41' // "A"
Raw String ES2015
String.raw`...`
Template literal without escape processing
String.raw`\n` // "\\n"
Example: String creation patterns
// Literal syntax (preferred)
const single = 'Hello';
const double = "World";
const template = `Hello ${name}`;
// String constructor (type conversion)
String(123); // "123"
String(true); // "true"
String(null); // "null"
String({a: 1}); // "[object Object]"
// Avoid String object (wrapper)
const bad = new String("text"); // typeof bad === "object" ❌
const good = "text"; // typeof good === "string" ✓
// Escape sequences
'Line 1\nLine 2'; // newline
'Tab\tseparated'; // tab
'She said "hi"'; // double quotes in single
"It's okay"; // single quote in double
'Can\'t escape'; // escaped single quote
// More escape sequences
'C:\\Users\\name'; // backslashes
'First\x20Second'; // space using hex
'\u00A9 2025'; // © 2025
'\u{1F4A9}'; // 💩 (requires code point syntax)
// Multiline strings
const multi = `Line 1
Line 2
Line 3`; // preserves newlines
const legacy = 'Line 1\n' +
'Line 2\n' +
'Line 3'; // old way
// Raw strings (no escape processing)
String.raw`C:\Users\name`; // "C:\\Users\\name"
String.raw`\n\t`; // "\\n\\t" (literal backslashes)
// Empty and whitespace
const empty = '';
const space = ' ';
const whitespace = ' \t\n ';
// Immutability
const str = 'hello';
str[0] = 'H'; // No effect - strings are immutable
str.toUpperCase(); // Returns "HELLO", str unchanged
Note: Strings are immutable primitives. Use single/double quotes
for simple strings; template literals for interpolation/multiline. Avoid new String() wrapper
objects.
10.2 String Methods and String Manipulation
Method
Syntax
Description
Returns
length
str.length
Number of UTF-16 code units (not characters!); astral chars count as 2
Number
charAt()
str.charAt(index)
Character at index; returns empty string if out of bounds
String
charCodeAt()
str.charCodeAt(index)
UTF-16 code unit (0-65535); NaN if out of bounds
Number
codePointAt()
str.codePointAt(index)
Full Unicode code point; handles astral characters correctly
Number
at() ES2022
str.at(index)
Character at index; negative indices from end; undefined if out of bounds
String | undefined
concat()
str.concat(str2, ...)
Join strings; prefer + or template literals for readability
String
slice()
str.slice(start, end)
Extract substring; negative indices from end; preferred method
String
substring()
str.substring(start, end)
Like slice but swaps args if start > end; no negative indices
String
substr() DEPRECATED
str.substr(start, length)
Extract by length; deprecated - use slice() instead
String
toUpperCase()
str.toUpperCase()
Convert to uppercase; locale-independent
String
toLowerCase()
str.toLowerCase()
Convert to lowercase; locale-independent
String
toLocaleUpperCase()
str.toLocaleUpperCase(locale)
Locale-aware uppercase (e.g., Turkish İ/I)
String
toLocaleLowerCase()
str.toLocaleLowerCase(locale)
Locale-aware lowercase
String
trim()
str.trim()
Remove whitespace from both ends
String
trimStart() / trimLeft()
str.trimStart()
Remove whitespace from start only
String
trimEnd() / trimRight()
str.trimEnd()
Remove whitespace from end only
String
padStart() ES2017
str.padStart(length, padStr)
Pad to length from start; default space
String
padEnd() ES2017
str.padEnd(length, padStr)
Pad to length from end; default space
String
repeat()
str.repeat(count)
Repeat string count times; count must be non-negative integer
String
Example: String manipulation methods
const str = 'Hello World';
// Access characters
str.charAt(0); // "H"
str[0]; // "H" (bracket notation)
str.at(0); // "H"
str.at(-1); // "d" (last character)
str.at(-2); // "l" (second to last)
// Character codes
str.charCodeAt(0); // 72 (UTF-16 code unit)
str.codePointAt(0); // 72 (Unicode code point)
// Emoji handling
const emoji = '😀';
emoji.length; // 2 ❌ (surrogate pair)
emoji.codePointAt(0); // 128512 ✓
emoji.charCodeAt(0); // 55357 ❌ (high surrogate only)
// Substrings
str.slice(0, 5); // "Hello"
str.slice(6); // "World"
str.slice(-5); // "World" (last 5)
str.slice(-5, -1); // "Worl"
str.substring(0, 5); // "Hello" (same as slice)
str.substring(5, 0); // "Hello" (swaps args!)
// Case conversion
'JavaScript'.toUpperCase(); // "JAVASCRIPT"
'JavaScript'.toLowerCase(); // "javascript"
// Turkish locale example
'i'.toLocaleUpperCase('tr'); // "İ" (dotted capital I)
'I'.toLocaleLowerCase('tr'); // "ı" (dotless small i)
// Whitespace removal
' hello '.trim(); // "hello"
' hello '.trimStart(); // "hello "
' hello '.trimEnd(); // " hello"
// Padding
'5'.padStart(3, '0'); // "005"
'123'.padStart(5, '0'); // "00123"
'hello'.padEnd(10, '.'); // "hello....."
const cardNum = '1234';
cardNum.padStart(16, '*'); // "************1234"
// Repeat
'*'.repeat(10); // "**********"
'abc'.repeat(3); // "abcabcabc"
// Concatenation
'Hello'.concat(' ', 'World'); // "Hello World"
'a' + 'b' + 'c'; // "abc" (preferred)
// Chaining
const result = ' HELLO '
.trim()
.toLowerCase()
.repeat(2)
.padStart(15, '.');
// "...hellohello"
Note: Use slice() over substring() for consistency with arrays. Avoid
deprecated substr(). Remember length counts UTF-16 code units, not characters.
10.3 Template Literals and Tag Functions
Feature
Syntax
Description
Example
Template Literal
`text`
Backticks; supports interpolation and multiline
`Hello ${name}`
Interpolation
${expression}
Embed any expression; converted to string via ToString
`Sum: ${a + b}`
Multiline
`line1\nline2`
Preserves actual newlines and indentation
`Line 1\nLine 2`
Tagged Template
tag`template`
Function processes template; receives strings and values separately
html`<div>${text}</div>`
String.raw
String.raw`text`
Built-in tag that returns raw string (no escape processing)
String.raw`\n` // "\\n"
Tag Function Signature
Receives strings array and interpolated values
Parameters
function tag(strings, ...values)
strings
Array of string literals; has .raw property for unprocessed strings
...values
Rest parameter with all interpolated expression results
Example: Template literals and tag functions
// Basic interpolation
const name = 'Alice';
const age = 25;
const msg = `Hello ${name}, you are ${age} years old`;
// "Hello Alice, you are 25 years old"
// Expressions
const a = 10, b = 20;
`Sum: ${a + b}`; // "Sum: 30"
`Comparison: ${a > b ? 'a' : 'b'}`; // "Comparison: b"
`Array: ${[1, 2, 3].join(', ')}`; // "Array: 1, 2, 3"
// Multiline
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`; // Preserves indentation and newlines
// Nesting
`Outer ${`Inner ${1 + 1}`} text`; // "Outer Inner 2 text"
// String.raw - no escape processing
String.raw`C:\Users\name`; // "C:\\Users\\name"
String.raw`Line 1\nLine 2`; // "Line 1\\nLine 2"
// Custom tag function - basic
function upper(strings, ...values) {
let result = '';
strings.forEach((str, i) => {
result += str;
if (i < values.length) {
result += String(values[i]).toUpperCase();
}
});
return result;
}
upper`Hello ${name}, age ${age}`;
// "Hello ALICE, age 25"
// Tag function - HTML escaping
function html(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] || '';
const escaped = String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
return result + str + escaped;
}, '');
}
const userInput = '<script>alert("xss")</script>';
html`<div>${userInput}</div>`;
// "<div><script>alert("xss")</script></div>"
// Tag function - SQL (parameterized queries)
function sql(strings, ...values) {
return {
query: strings.join('?'),
params: values
};
}
const userId = 123;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// {query: "SELECT * FROM users WHERE id = ?", params: [123]}
// Tag function - i18n translation
function t(strings, ...values) {
const key = strings.join('{}');
// Look up translation for key
return translate(key, values);
}
t`Welcome ${name}!`; // Translates "Welcome {}!" with name
// Access raw strings
function logger(strings, ...values) {
console.log('Processed:', strings);
console.log('Raw:', strings.raw);
console.log('Values:', values);
}
logger`Line 1\nLine 2`;
// Processed: ["Line 1
// Line 2"]
// Raw: ["Line 1\\nLine 2"]
// Values: []
// Empty interpolations
`a${null}b`; // "anullb"
`a${undefined}b`; // "aundefinedb"
`a${''}b`; // "ab"
Note: Tagged templates enable custom string processing (HTML escaping, i18n, SQL, styling). The
strings.raw property provides unprocessed strings. All interpolated values are converted to
strings.
10.4 String Interpolation and Expression Embedding
Pattern
Syntax
Description
Example
Variable
${variable}
Insert variable value; converts to string
`Hello ${name}`
Expression
${expr}
Any valid expression; evaluated and converted to string
`Sum: ${a + b}`
Function Call
${fn()}
Call function and insert return value
`Time: ${getTime()}`
Ternary
${cond ? a : b}
Conditional expressions inline
`${online ? '🟢' : '🔴'}`
Logical AND
${cond && value}
Short-circuit conditional rendering; false becomes "false" string
`${hasError && errorMsg}`
Logical OR
${val || default}
Fallback value; beware of 0, '', false
`${name || 'Guest'}`
Nullish Coalescing
${val ?? default}
Fallback only for null/undefined; preserves 0, '', false
`${count ?? 0}`
Array Method
${arr.method()}
Array operations inline
`Items: ${arr.join(', ')}`
Object Property
${obj.prop}
Access nested properties
`Name: ${user.name}`
Nested Template
${`inner ${val}`}
Template literals can be nested
`Outer ${`inner ${x}`}`
Example: String interpolation patterns
const user = {name: 'Alice', age: 25, online: true};
const items = ['apple', 'banana', 'orange'];
const count = 0;
// Basic variable
`Username: ${user.name}`; // "Username: Alice"
// Arithmetic expressions
`Total: ${(99.99 * 1.1).toFixed(2)}`; // "Total: $109.99"
`Progress: ${(completed / total * 100).toFixed(1)}%`;
// Function calls
`Generated at ${new Date().toISOString()}`;
`Random: ${Math.random().toFixed(4)}`;
// Ternary operator
`Status: ${user.online ? '🟢 Online' : '🔴 Offline'}`;
`You have ${count === 0 ? 'no' : count} items`;
// Logical AND (conditional content)
`${user.admin && 'Admin Panel'}`;
// If admin: "Admin Panel", else: "false" ❌
// Better: use ternary for boolean
`${user.admin ? 'Admin Panel' : ''}`;
// If admin: "Admin Panel", else: ""
// Logical OR (fallback)
`Hello ${user.nickname || user.name}`;
`Count: ${count || 'N/A'}`; // ❌ 0 becomes 'N/A'!
// Nullish coalescing (better fallback)
`Count: ${count ?? 'N/A'}`; // ✓ 0 stays 0
`Name: ${user.name ?? 'Anonymous'}`;
// Array methods
`Items: ${items.join(', ')}`; // "Items: apple, banana, orange"
`Count: ${items.length}`;
`First: ${items[0]}`;
// Array rendering with map
`<ul>${items.map(item => `<li>${item}</li>`).join('')}</ul>`;
// "<ul><li>apple</li><li>banana</li><li>orange</li></ul>"
// Object methods
`User: ${JSON.stringify(user)}`;
`Keys: ${Object.keys(user).join(', ')}`;
// Nested templates
const className = `btn ${user.online ? 'btn-success' : 'btn-danger'}`;
`<button class="${className}">${user.name}</button>`;
// Complex expressions
const price = 99.99;
const discount = 0.15;
const tax = 0.08;
`Final: ${(price * (1 - discount) * (1 + tax)).toFixed(2)}`;
// Method chaining
`Result: ${str.trim().toLowerCase().split(' ').join('-')}`;
// Type coercion
`Number: ${123}`; // "Number: 123"
`Boolean: ${true}`; // "Boolean: true"
`Null: ${null}`; // "Null: null"
`Undefined: ${undefined}`; // "Undefined: undefined"
`Object: ${{}}`; // "Object: [object Object]"
`Array: ${[1, 2]}`; // "Array: 1,2"
// Multi-line with interpolation
const html = `
<div class="user-card ${user.online ? 'online' : 'offline'}">
<h2>${user.name}</h2>
<p>Age: ${user.age}</p>
<p>Status: ${user.online ? '🟢' : '🔴'}</p>
</div>
`;
// Escaping interpolation
const literal = String.raw`Not interpolated: \${variable}`;
// "Not interpolated: ${variable}"
Warning: Logical AND ${cond && val} converts false to "false" string. Use ternary
${cond ? val : ''} for conditional rendering. Prefer ?? over || for
fallbacks.
10.5 Unicode Handling and Character Encoding
Method/Feature
Syntax
Description
Example
length Property
str.length
UTF-16 code units, not characters; astral symbols count as 2
'😀'.length // 2
Code Point Escape
\u{codepoint}
1-6 hex digits; supports full Unicode (0-10FFFF)
'\u{1F600}' // 😀
BMP Escape
\uXXXX
4 hex digits; Basic Multilingual Plane only (0-FFFF)
'\u00A9' // ©
String.fromCharCode()
String.fromCharCode(code, ...)
Create from UTF-16 code units; astral chars need two codes (surrogate pair)
String.fromCharCode(65) // "A"
String.fromCodePoint()
String.fromCodePoint(point, ...)
Create from Unicode code points; handles astral chars correctly
String.fromCodePoint(0x1F600) // "😀"
codePointAt()
str.codePointAt(index)
Get full Unicode code point at position
'😀'.codePointAt(0) // 128512
charCodeAt()
str.charCodeAt(index)
Get UTF-16 code unit; only half of astral char (high surrogate)
'😀'.charCodeAt(0) // 55357
normalize()
str.normalize(form)
Unicode normalization: NFC, NFD, NFKC, NFKD; for comparison
'café'.normalize('NFC')
localeCompare()
str.localeCompare(str2, locale)
Locale-aware string comparison; handles accents, case correctly
'ä'.localeCompare('z', 'de')
Spread/Array.from
[...str]
Iterate by code points, not code units; handles emoji correctly
[...'😀🎉'].length // 2
for...of Loop
for (const char of str)
Iterates by Unicode code points; emoji-safe
for (const c of '😀')
Example: Unicode handling
// BMP characters (single code unit)
const ascii = 'A';
ascii.length; // 1
ascii.charCodeAt(0); // 65
ascii.codePointAt(0); // 65
const copyright = '\u00A9'; // ©
copyright.length; // 1
// Astral plane (emoji, historic scripts)
const emoji = '😀';
emoji.length; // 2 ❌ (surrogate pair!)
emoji.charCodeAt(0); // 55357 (high surrogate)
emoji.charCodeAt(1); // 56832 (low surrogate)
emoji.codePointAt(0); // 128512 ✓ (full code point)
// Creating from code points
String.fromCharCode(65); // "A"
String.fromCharCode(0x00A9); // "©"
// Emoji requires fromCodePoint
String.fromCodePoint(0x1F600); // "😀"
String.fromCharCode(0x1F600); // ❌ Wrong!
// Surrogate pair manually (don't do this!)
String.fromCharCode(0xD83D, 0xDE00); // "😀" (works but use fromCodePoint)
// Code point escape syntax
'\u{1F600}'; // "😀"
'\u{1F4A9}'; // "💩"
'\u{1F680}'; // "🚀"
// Length issues with emoji
'hello'.length; // 5 ✓
'😀'.length; // 2 ❌
'👨👩👧👦'.length; // 11 ❌ (family emoji with ZWJ)
// Count actual characters
[...'hello'].length; // 5
[...'😀'].length; // 1 ✓
[...'😀🎉'].length; // 2 ✓
// However, combining characters still tricky
[...'👨👩👧👦'].length; // 7 (4 people + 3 ZWJ)
// Iterate correctly
for (const char of '😀🎉') {
console.log(char); // "😀", "🎉" (correct!)
}
// Split fails with emoji
'😀🎉'.split(''); // ['�','�','�','�'] ❌
[...'😀🎉']; // ['😀','🎉'] ✓
// Unicode normalization
const e1 = '\u00E9'; // é (single code point)
const e2 = '\u0065\u0301'; // é (e + combining acute accent)
e1 === e2; // false ❌
e1.length; // 1
e2.length; // 2
e1.normalize() === e2.normalize(); // true ✓
// Forms: NFC (composed), NFD (decomposed)
'café'.normalize('NFC').length; // 4
'café'.normalize('NFD').length; // 5 (e + combining accent)
// Locale-aware comparison
const strings = ['ä', 'z', 'a'];
strings.sort(); // ['a', 'z', 'ä'] ❌
strings.sort((a, b) => a.localeCompare(b, 'de'));
// ['a', 'ä', 'z'] ✓ (German collation)
'ä'.localeCompare('z', 'de'); // -1 (ä comes before z)
'ä'.localeCompare('z', 'sv'); // 1 (ä comes after z in Swedish)
// Case comparison with locale
'i'.toLocaleUpperCase('en'); // "I"
'i'.toLocaleUpperCase('tr'); // "İ" (dotted capital I in Turkish)
// Real character count (approximate)
function graphemeLength(str) {
return [...new Intl.Segmenter().segment(str)].length;
}
graphemeLength('😀'); // 1
graphemeLength('👨👩👧👦'); // 1 (treats family as one grapheme)
Warning: length counts UTF-16 code units, not characters. Use
[...str].length for code point count. For grapheme clusters (with ZWJ, combining marks), use
Intl.Segmenter.
10.6 String Regular Expression Integration
Method
Syntax
Description
Returns
match()
str.match(regexp)
Find matches; with /g returns all matches, without returns match details
Array | null
matchAll() ES2020
str.matchAll(regexp)
Iterator of all matches with capture groups; regexp must have /g flag
Iterator
search()
str.search(regexp)
Find index of first match; ignores /g flag
Index | -1
replace()
str.replace(regexp|str, newStr|fn)
Replace matches; without /g replaces first only
String
replaceAll() ES2021
str.replaceAll(str|regexp, newStr|fn)
Replace all occurrences; regexp must have /g or use string
String
split()
str.split(separator, limit)
Split by string or regexp; optional limit on array size
Array
startsWith()
str.startsWith(search, pos)
Check if starts with string; optional position; no regexp
Boolean
endsWith()
str.endsWith(search, length)
Check if ends with string; optional length limit; no regexp
Boolean
includes()
str.includes(search, pos)
Check if contains string; optional start position; no regexp
Boolean
indexOf()
str.indexOf(search, pos)
Find first occurrence; case-sensitive; no regexp
Index | -1
lastIndexOf()
str.lastIndexOf(search, pos)
Find last occurrence; searches backwards; no regexp
Index | -1
Example: String RegExp integration
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;month>/
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;day>/
const str = 'The quick brown fox jumps over the lazy dog';
// match - without /g (first match with details)
str.match(/quick/);
// ["quick", index: 4, input: "...", groups: undefined]
str.match(/(\w+) (\w+)/);
// ["The quick", "The", "quick", index: 0, ...]
// match - with /g (all matches, no details)
str.match(/\w+/g);
// ["The", "quick", "brown", "fox", ...]
str.match(/o/g); // ["o", "o", "o", "o"]
// matchAll - all matches with details
const regex = /t(\w+)/gi;
const matches = [...str.matchAll(regex)];
matches[0]; // ["The", "he", index: 0, ...]
matches[1]; // ["the", "he", index: 31, ...]
// matchAll with named groups
const pattern = /(?<word>\w+) (?<num>\d+)/g;
const text = 'item 1, thing 2';
for (const match of text.matchAll(pattern)) {
console.log(match.groups.word, match.groups.num);
// "item" "1", "thing" "2"
}
// search - find position
str.search(/brown/); // 10
str.search(/cat/); // -1
str.search(/THE/i); // 0 (case-insensitive)
// replace - string or regexp
str.replace('dog', 'cat');
// "The quick brown fox jumps over the lazy cat"
str.replace(/the/gi, 'a');
// "a quick brown fox jumps over a lazy dog"
// replace with function
str.replace(/\b\w{4}\b/g, match => match.toUpperCase());
// "The QUICK brown fox JUMPS OVER the LAZY dog"
// replace with capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// replace with named groups
'2025-12-17'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
#x3C;year>'
); // "12/17/2025"
// replaceAll - all occurrences
'aaa'.replace('a', 'b'); // "baa"
'aaa'.replaceAll('a', 'b'); // "bbb"
'a-b-c'.replaceAll('-', '_'); // "a_b_c"
// split by regexp
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split with limit
'a-b-c-d'.split('-', 2); // ["a", "b"]
// includes, startsWith, endsWith (no regexp!)
str.includes('fox'); // true
str.includes('cat'); // false
str.startsWith('The'); // true
str.startsWith('the'); // false (case-sensitive)
str.endsWith('dog'); // true
str.endsWith('cat'); // false
// indexOf, lastIndexOf
str.indexOf('o'); // 12 (first 'o')
str.lastIndexOf('o'); // 41 (last 'o')
str.indexOf('the'); // -1 (case-sensitive)
str.toLowerCase().indexOf('the'); // 0
// Check from position
str.startsWith('quick', 4); // true (at index 4)
str.includes('fox', 20); // false (search starts at 20)
// Case-insensitive search
const lower = str.toLowerCase();
lower.includes('THE'.toLowerCase()); // true
// Or use regexp with /i
/THE/i.test(str); // true
Note: Use match() for simple searches; matchAll() for all matches
with details; replace() with functions for complex transformations.
includes/startsWith/endsWith don't support regexp.
Section 10 Summary
- Creation: Use single/double quotes for simple strings; template literals
`...` for interpolation/multiline; escape sequences \n \t; Unicode
\u{...} for full range
- Methods:
slice() for substrings (supports negative indices);
trim/padStart/padEnd for formatting; toUpperCase/toLowerCase for case; all return
new strings (immutable)
- Template literals:
`${expr}` interpolation; multiline
preserves formatting; tagged templates tag`...` for custom processing (HTML escape, i18n, SQL)
- Interpolation: Any expression; use ternary
? : not
&& for conditionals; ?? for null/undefined fallback; beware
|| with 0/false
- Unicode:
length counts UTF-16 units not characters;
[...str] for code points; codePointAt/fromCodePoint for emoji;
normalize() for comparison
- RegExp:
match/matchAll find patterns;
replace/replaceAll modify; split divides;
includes/startsWith/endsWith check (no regexp); search finds index
11. Regular Expressions and Pattern Matching
11.1 RegExp Literal and Constructor Syntax
Syntax
Form
Description
Example
Literal Syntax
/pattern/flags
Most common; compiled at parse time; cannot use variables in pattern
/hello/i
Constructor
new RegExp(pattern, flags)
Dynamic patterns; compiled at runtime; pattern is string (escape backslashes!)
new RegExp('\\d+', 'g')
Constructor (no new)
RegExp(pattern, flags)
Same as with new; returns RegExp instance
RegExp('test')
From RegExp
new RegExp(regexp, flags)
Clone with optional new flags; ES2015+
new RegExp(/test/i, 'g')
Property
Type
Description
Example
source
String
Pattern text (without delimiters and flags)
/abc/i.source // "abc"
flags
String
All flags as string; alphabetically sorted
/a/gi.flags // "gi"
global
Boolean
Has g flag
/a/g.global // true
ignoreCase
Boolean
Has i flag
/a/i.ignoreCase // true
multiline
Boolean
Has m flag
/a/m.multiline // true
dotAll
Boolean
Has s flag (. matches newlines)
/a/s.dotAll // true
unicode
Boolean
Has u flag
/a/u.unicode // true
sticky
Boolean
Has y flag
/a/y.sticky // true
lastIndex
Number
Start position for next match; used with g or y flags
regex.lastIndex = 0
Example: RegExp creation and properties
// Literal syntax (preferred)
const literal = /hello/i;
const pattern = /\d{3}-\d{4}/;
// Constructor - dynamic patterns
const word = 'test';
const dynamic = new RegExp(word, 'gi');
// Matches 'test' case-insensitively, globally
// Constructor - escape backslashes!
const wrong = new RegExp('\d+'); // ❌ \d becomes 'd'
const correct = new RegExp('\\d+'); // ✓ \\d becomes \d
// Or use String.raw
const regex = new RegExp(String.raw`\d+`);
// Clone with new flags
const original = /test/i;
const clone = new RegExp(original, 'g');
// Pattern: "test", flags: "g" (not "gi")
// Properties
const re = /hello/gi;
re.source; // "hello"
re.flags; // "gi"
re.global; // true
re.ignoreCase; // true
re.multiline; // false
// lastIndex (stateful with g or y flag)
const globalRe = /test/g;
globalRe.lastIndex; // 0 (initial)
globalRe.exec('test test');
globalRe.lastIndex; // 4 (after first match)
// Reset lastIndex
globalRe.lastIndex = 0;
// Create from string (user input)
function createRegex(userPattern, options = {}) {
const flags =
(options.global ? 'g' : '') +
(options.ignoreCase ? 'i' : '') +
(options.multiline ? 'm' : '');
try {
return new RegExp(userPattern, flags);
} catch (e) {
console.error('Invalid regex:', e.message);
return null;
}
}
// Escape special characters for literal matching
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\
// Literal syntax (preferred)
const literal = /hello/i;
const pattern = /\d{3}-\d{4}/;
// Constructor - dynamic patterns
const word = 'test';
const dynamic = new RegExp(word, 'gi');
// Matches 'test' case-insensitively, globally
// Constructor - escape backslashes!
const wrong = new RegExp('\d+'); // ❌ \d becomes 'd'
const correct = new RegExp('\\d+'); // ✓ \\d becomes \d
// Or use String.raw
const regex = new RegExp(String.raw`\d+`);
// Clone with new flags
const original = /test/i;
const clone = new RegExp(original, 'g');
// Pattern: "test", flags: "g" (not "gi")
// Properties
const re = /hello/gi;
re.source; // "hello"
re.flags; // "gi"
re.global; // true
re.ignoreCase; // true
re.multiline; // false
// lastIndex (stateful with g or y flag)
const globalRe = /test/g;
globalRe.lastIndex; // 0 (initial)
globalRe.exec('test test');
globalRe.lastIndex; // 4 (after first match)
// Reset lastIndex
globalRe.lastIndex = 0;
// Create from string (user input)
function createRegex(userPattern, options = {}) {
const flags =
(options.global ? 'g' : '') +
(options.ignoreCase ? 'i' : '') +
(options.multiline ? 'm' : '');
try {
return new RegExp(userPattern, flags);
} catch (e) {
console.error('Invalid regex:', e.message);
return null;
}
}
// Escape special characters for literal matching
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const userInput = 'hello.world';
const escaped = escapeRegex(userInput); // "hello\\.world"
const safe = new RegExp(escaped); // matches literal "hello.world"
#x26;");
}
const userInput = 'hello.world';
const escaped = escapeRegex(userInput); // "hello\\.world"
const safe = new RegExp(escaped); // matches literal "hello.world"
Note: Use literals /pattern/flags for static patterns (better performance);
constructor for dynamic patterns. Remember to double-escape backslashes in strings: '\\d' not
'\d'.
11.2 RegExp Flags and Global Modifiers
Flag
Name
Description
Effect
g
Global
Find all matches, not just first; affects exec(), test(), match(), replace()
Multiple matches
i
Ignore Case
Case-insensitive matching; [a-z] matches both cases
/a/i matches 'A' and 'a'
m
Multiline
^ and $ match line boundaries, not just string start/end
^word matches after \n
s ES2018
DotAll
. (dot) matches newlines; normally dot excludes \n \r \u2028 \u2029
/.+/s matches across lines
u ES2015
Unicode
Full Unicode support; astral characters, Unicode escapes, properties
\u{1F600} and \p{...}
y ES2015
Sticky
Match must start at lastIndex; no skipping ahead
For tokenizers/parsers
d ES2022
Indices
Capture group indices in .indices property of match result
match.indices[0]
Example: Flag behaviors
const str = 'Hello World\nHello Again';
// g - global (all matches)
str.match(/hello/i); // ["Hello"] (first only)
str.match(/hello/gi); // ["Hello", "Hello"] (all)
/\d+/g.test('123'); // true
/\d+/g.test('123'); // false (lastIndex moved!)
// i - ignore case
/hello/.test('HELLO'); // false
/hello/i.test('HELLO'); // true
// m - multiline (^ $ match line boundaries)
const multi = 'line1\nline2\nline3';
multi.match(/^line/g); // ["line"] (only first)
multi.match(/^line/gm); // ["line", "line", "line"] (each line)
/world$/.test('hello\nworld'); // true (end of string)
/world$/m.test('world\nhello'); // true (end of line)
// s - dotAll (. matches newlines)
/.+/.test('hello\nworld'); // false (. stops at \n)
/.+/s.test('hello\nworld'); // true (. matches \n)
'a\nb'.match(/a.b/); // null
'a\nb'.match(/a.b/s); // ["a\nb"]
// u - unicode (proper astral character handling)
'😀'.match(/./); // ["�"] ❌ (high surrogate only)
'😀'.match(/./u); // ["😀"] ✓
// Unicode property escapes (requires u flag)
/\p{Emoji}/u.test('😀'); // true
/\p{Script=Greek}/u.test('α'); // true
/\p{Letter}/u.test('A'); // true
// Count code points, not code units
'😀'.length; // 2
'😀'.match(/./gu); // ["😀"] (1 match)
'😀'.match(/./g); // ["�","�"] (2 matches)
// y - sticky (match at exact position)
const sticky = /\d+/y;
sticky.lastIndex = 0;
sticky.exec('123 456'); // ["123"]
sticky.lastIndex; // 3
sticky.exec('123 456'); // null (position 3 is space)
sticky.lastIndex; // 0 (reset on failure)
// Without y, would skip ahead and match 456
const notSticky = /\d+/g;
notSticky.lastIndex = 3;
notSticky.exec('123 456'); // ["456"]
// d - indices (capture group positions)
const indicesRe = /(\d+)-(\d+)/d;
const match = indicesRe.exec('id: 123-456');
match[1]; // "123"
match[2]; // "456"
match.indices[1]; // [4, 7] (position of "123")
match.indices[2]; // [8, 11] (position of "456")
// Combine flags
const combined = /pattern/gimsu;
combined.flags; // "gimsu" (alphabetically sorted)
Warning: Global g flag makes RegExp stateful
(lastIndex). Reset with regex.lastIndex = 0 or use string methods. Always use u flag
for Unicode text!
11.3 Pattern Matching Methods (test, exec, match)
Method
Syntax
Description
Returns
test()
regex.test(str)
Check if pattern matches; fastest for existence check
Boolean
exec()
regex.exec(str)
Get match details with capture groups; use in loop with /g flag
Array | null
match()
str.match(regex)
Without /g: like exec(); with /g: array of all matches (no details)
Array | null
matchAll() ES2020
str.matchAll(regex)
Iterator of all matches with full details; regex must have /g
Iterator
Example: Pattern matching methods
const str = 'The year 2025 and 2026';
// test() - simple existence check
/\d+/.test(str); // true
/cat/.test(str); // false
// Use for validation
function isEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// exec() - detailed match info
const yearRe = /(\d{4})/;
const match = yearRe.exec(str);
match[0]; // "2025" (full match)
match[1]; // "2025" (capture group 1)
match.index; // 9 (position)
match.input; // original string
// exec() with /g flag (iterate)
const yearReGlobal = /(\d{4})/g;
let m;
while ((m = yearReGlobal.exec(str)) !== null) {
console.log(m[1], 'at', m.index);
// "2025" at 9
// "2026" at 18
}
// match() without /g (same as exec)
str.match(/(\d{4})/);
// ["2025", "2025", index: 9, input: "...", groups: undefined]
// match() with /g (all matches, no details)
str.match(/\d{4}/g); // ["2025", "2026"]
str.match(/\d+/g); // ["2025", "2026"]
// matchAll() - best of both (ES2020+)
const years = str.matchAll(/(\d{4})/g);
for (const match of years) {
console.log(match[1], 'at', match.index);
// "2025" at 9
// "2026" at 18
}
// Convert to array
const allMatches = [...str.matchAll(/(\d{4})/g)];
// Named capture groups
const phoneRe = /(?<area>\d{3})-(?<exchange>\d{3})-(?<number>\d{4})/;
const phone = '555-123-4567';
const phoneMatch = phone.match(phoneRe);
phoneMatch[1]; // "555"
phoneMatch.groups.area; // "555"
phoneMatch.groups.exchange; // "123"
phoneMatch.groups.number; // "4567"
// Multiple patterns
const email = 'user@example.com';
const emailRe = /^(?<user>[^@]+)@(?<domain>[^@]+)$/;
const emailMatch = email.match(emailRe);
emailMatch.groups.user; // "user"
emailMatch.groups.domain; // "example.com"
// No match returns null
'abc'.match(/\d+/); // null
/\d+/.test('abc'); // false
/\d+/.exec('abc'); // null
// Global test() gotcha (stateful!)
const re = /test/g;
re.test('test test'); // true (first call)
re.test('test test'); // true (second call)
re.test('test test'); // false! (lastIndex past end)
re.lastIndex; // 0 (reset)
// Solution: don't reuse global regex for test()
function hasDigit(str) {
return /\d/.test(str); // no /g, always works
}
// Or reset lastIndex
function hasPattern(str, regex) {
regex.lastIndex = 0;
return regex.test(str);
}
Note: Use test() for boolean checks; match() for simple extraction;
matchAll() for all matches with details. Avoid test() with global regex in loops
(stateful).
11.4 String RegExp Methods (replace, split, search)
Method
Syntax
Description
Returns
replace()
str.replace(regex, newStr|fn)
Replace first match (or all with /g); newStr can use $1, $2, etc.
String
replaceAll() ES2021
str.replaceAll(str|regex, newStr|fn)
Replace all occurrences; regex must have /g flag
String
split()
str.split(regex, limit)
Split string by pattern; optional limit; capture groups included in result
Array
search()
str.search(regex)
Find index of first match; ignores /g flag; like indexOf for regex
Index | -1
Replacement Patterns (in replace newStr argument)
Pattern
Inserts
Example
Result
$$
Literal $
'$10'.replace(/\d+/, '$$&')
"$&10"
$&
Entire match
'cat'.replace(/\w+/, '[$&]')
"[cat]"
$`
Text before match
'a-b'.replace(/-/, '$`')
"aab"
$'
Text after match
'a-b'.replace(/-/, "$'")
"abb"
$n
Capture group n (1-99)
'John Doe'.replace(/(\w+) (\w+)/, '$2, $1')
"Doe, John"
$<name>
Named capture group
'2025-12-17'.replace(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, '$<m>/$<d>/$<y>')
"12/17/2025"
Example: String RegExp methods
|$\')'); // "a(a|b)b"
// Capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// Named groups
const date = '2025-12-17';
date.replace(
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/,
'
const str = 'Hello World, hello again';
// replace - first match
str.replace('hello', 'hi'); // "Hello World, hi again"
str.replace(/hello/, 'hi'); // "Hello World, hi again"
// replace - all matches (with /g)
str.replace(/hello/gi, 'hi'); // "hi World, hi again"
// replaceAll - simpler for strings
'aaa'.replace(/a/g, 'b'); // "bbb"
'aaa'.replaceAll('a', 'b'); // "bbb"
// Replacement patterns
'test'.replace(/test/, '[$&]'); // "[test]"
'a-b'.replace(/-/, '($`|$\')'); // "a(a|b)b"
// Capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// Named groups
const date = '2025-12-17';
date.replace(
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// Replace with function
str.replace(/\b\w+\b/g, match => match.toUpperCase());
// "HELLO WORLD, HELLO AGAIN"
// Function receives: match, p1, p2, ..., offset, string, groups
'1 plus 2 equals 3'.replace(/(\d+)/g, (match, p1, offset) => {
return parseInt(p1) * 2;
}); // "2 plus 4 equals 6"
// Advanced replacements
const text = 'Price: $10.50, Total: $20.00';
text.replace(/\$(\d+\.\d+)/g, (match, price) => {
return `$${(parseFloat(price) * 1.1).toFixed(2)}`;
}); // "Price: $11.55, Total: $22.00"
// split - by regex
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split - with limit
'a-b-c-d'.split(/-/, 2); // ["a", "b"]
// split - capture groups included in result
'a1b2c'.split(/(\d)/);
// ["a", "1", "b", "2", "c"]
// split - whitespace
' hello world '.split(/\s+/);
// ["", "hello", "world", ""]
' hello world '.trim().split(/\s+/);
// ["hello", "world"]
// search - find index
str.search(/world/i); // 6
str.search(/cat/); // -1
// search vs indexOf
'hello'.indexOf('l'); // 2 (first 'l')
'hello'.search(/l/); // 2 (first 'l')
// search with patterns
'abc123xyz'.search(/\d/); // 3 (first digit)
'test@email.com'.search(/@/); // 4
// Use cases
// 1. Format phone numbers
function formatPhone(phone) {
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
formatPhone('5551234567'); // "(555) 123-4567"
// 2. Sanitize HTML
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. Slugify
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
slugify('Hello World!'); // "hello-world"
// 4. Extract data
const email = 'Contact: user@example.com';
const match = email.match(/[\w.]+@[\w.]+/);
const emailAddr = match ? match[0] : null;
#x3C;month>/
const str = 'Hello World, hello again';
// replace - first match
str.replace('hello', 'hi'); // "Hello World, hi again"
str.replace(/hello/, 'hi'); // "Hello World, hi again"
// replace - all matches (with /g)
str.replace(/hello/gi, 'hi'); // "hi World, hi again"
// replaceAll - simpler for strings
'aaa'.replace(/a/g, 'b'); // "bbb"
'aaa'.replaceAll('a', 'b'); // "bbb"
// Replacement patterns
'test'.replace(/test/, '[$&]'); // "[test]"
'a-b'.replace(/-/, '($`|$\')'); // "a(a|b)b"
// Capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// Named groups
const date = '2025-12-17';
date.replace(
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// Replace with function
str.replace(/\b\w+\b/g, match => match.toUpperCase());
// "HELLO WORLD, HELLO AGAIN"
// Function receives: match, p1, p2, ..., offset, string, groups
'1 plus 2 equals 3'.replace(/(\d+)/g, (match, p1, offset) => {
return parseInt(p1) * 2;
}); // "2 plus 4 equals 6"
// Advanced replacements
const text = 'Price: $10.50, Total: $20.00';
text.replace(/\$(\d+\.\d+)/g, (match, price) => {
return `$${(parseFloat(price) * 1.1).toFixed(2)}`;
}); // "Price: $11.55, Total: $22.00"
// split - by regex
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split - with limit
'a-b-c-d'.split(/-/, 2); // ["a", "b"]
// split - capture groups included in result
'a1b2c'.split(/(\d)/);
// ["a", "1", "b", "2", "c"]
// split - whitespace
' hello world '.split(/\s+/);
// ["", "hello", "world", ""]
' hello world '.trim().split(/\s+/);
// ["hello", "world"]
// search - find index
str.search(/world/i); // 6
str.search(/cat/); // -1
// search vs indexOf
'hello'.indexOf('l'); // 2 (first 'l')
'hello'.search(/l/); // 2 (first 'l')
// search with patterns
'abc123xyz'.search(/\d/); // 3 (first digit)
'test@email.com'.search(/@/); // 4
// Use cases
// 1. Format phone numbers
function formatPhone(phone) {
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
formatPhone('5551234567'); // "(555) 123-4567"
// 2. Sanitize HTML
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. Slugify
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
slugify('Hello World!'); // "hello-world"
// 4. Extract data
const email = 'Contact: user@example.com';
const match = email.match(/[\w.]+@[\w.]+/);
const emailAddr = match ? match[0] : null;
#x3C;day>/
const str = 'Hello World, hello again';
// replace - first match
str.replace('hello', 'hi'); // "Hello World, hi again"
str.replace(/hello/, 'hi'); // "Hello World, hi again"
// replace - all matches (with /g)
str.replace(/hello/gi, 'hi'); // "hi World, hi again"
// replaceAll - simpler for strings
'aaa'.replace(/a/g, 'b'); // "bbb"
'aaa'.replaceAll('a', 'b'); // "bbb"
// Replacement patterns
'test'.replace(/test/, '[$&]'); // "[test]"
'a-b'.replace(/-/, '($`|$\')'); // "a(a|b)b"
// Capture groups
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1');
// "Smith, John"
// Named groups
const date = '2025-12-17';
date.replace(
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/,
'$<month>/$<day>/$<year>'
); // "12/17/2025"
// Replace with function
str.replace(/\b\w+\b/g, match => match.toUpperCase());
// "HELLO WORLD, HELLO AGAIN"
// Function receives: match, p1, p2, ..., offset, string, groups
'1 plus 2 equals 3'.replace(/(\d+)/g, (match, p1, offset) => {
return parseInt(p1) * 2;
}); // "2 plus 4 equals 6"
// Advanced replacements
const text = 'Price: $10.50, Total: $20.00';
text.replace(/\$(\d+\.\d+)/g, (match, price) => {
return `$${(parseFloat(price) * 1.1).toFixed(2)}`;
}); // "Price: $11.55, Total: $22.00"
// split - by regex
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split - with limit
'a-b-c-d'.split(/-/, 2); // ["a", "b"]
// split - capture groups included in result
'a1b2c'.split(/(\d)/);
// ["a", "1", "b", "2", "c"]
// split - whitespace
' hello world '.split(/\s+/);
// ["", "hello", "world", ""]
' hello world '.trim().split(/\s+/);
// ["hello", "world"]
// search - find index
str.search(/world/i); // 6
str.search(/cat/); // -1
// search vs indexOf
'hello'.indexOf('l'); // 2 (first 'l')
'hello'.search(/l/); // 2 (first 'l')
// search with patterns
'abc123xyz'.search(/\d/); // 3 (first digit)
'test@email.com'.search(/@/); // 4
// Use cases
// 1. Format phone numbers
function formatPhone(phone) {
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
formatPhone('5551234567'); // "(555) 123-4567"
// 2. Sanitize HTML
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. Slugify
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
slugify('Hello World!'); // "hello-world"
// 4. Extract data
const email = 'Contact: user@example.com';
const match = email.match(/[\w.]+@[\w.]+/);
const emailAddr = match ? match[0] : null;
#x3C;year>'
); // "12/17/2025"
// Replace with function
str.replace(/\b\w+\b/g, match => match.toUpperCase());
// "HELLO WORLD, HELLO AGAIN"
// Function receives: match, p1, p2, ..., offset, string, groups
'1 plus 2 equals 3'.replace(/(\d+)/g, (match, p1, offset) => {
return parseInt(p1) * 2;
}); // "2 plus 4 equals 6"
// Advanced replacements
const text = 'Price: $10.50, Total: $20.00';
text.replace(/\$(\d+\.\d+)/g, (match, price) => {
return `${(parseFloat(price) * 1.1).toFixed(2)}`;
}); // "Price: $11.55, Total: $22.00"
// split - by regex
'a1b2c3'.split(/\d/); // ["a", "b", "c", ""]
'one,two;three:four'.split(/[,;:]/);
// ["one", "two", "three", "four"]
// split - with limit
'a-b-c-d'.split(/-/, 2); // ["a", "b"]
// split - capture groups included in result
'a1b2c'.split(/(\d)/);
// ["a", "1", "b", "2", "c"]
// split - whitespace
' hello world '.split(/\s+/);
// ["", "hello", "world", ""]
' hello world '.trim().split(/\s+/);
// ["hello", "world"]
// search - find index
str.search(/world/i); // 6
str.search(/cat/); // -1
// search vs indexOf
'hello'.indexOf('l'); // 2 (first 'l')
'hello'.search(/l/); // 2 (first 'l')
// search with patterns
'abc123xyz'.search(/\d/); // 3 (first digit)
'test@email.com'.search(/@/); // 4
// Use cases
// 1. Format phone numbers
function formatPhone(phone) {
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
formatPhone('5551234567'); // "(555) 123-4567"
// 2. Sanitize HTML
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. Slugify
function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
slugify('Hello World!'); // "hello-world"
// 4. Extract data
const email = 'Contact: user@example.com';
const match = email.match(/[\w.]+@[\w.]+/);
const emailAddr = match ? match[0] : null;
Note: Use replace() with functions for complex transformations.
replaceAll() requires /g flag for regex. split() with capture groups includes
delimiters in result.
11.5 Character Classes and Quantifiers
Pattern
Matches
Description
Example
.
Any character
Except newline (unless /s flag); use [\s\S] or /s for any
/a.c/.test('abc') // true
\d
Digit [0-9]
ASCII digits only; use \p{N} with /u for Unicode digits
/\d+/.test('123') // true
\D
Non-digit [^0-9]
Anything except ASCII digits
/\D/.test('a') // true
\w
Word [A-Za-z0-9_]
Alphanumeric + underscore; ASCII only
/\w+/.test('hello') // true
\W
Non-word [^\w]
Anything except word characters
/\W/.test('@') // true
\s
Whitespace
Space, tab, newline, etc.: [ \t\n\r\f\v]
/\s+/.test('a b') // true
\S
Non-whitespace
Anything except whitespace
/\S/.test('a') // true
[abc]
Character set
Any single character in set; can use ranges
/[aeiou]/.test('e') // true
[^abc]
Negated set
Any character NOT in set
/[^0-9]/.test('a') // true
[a-z]
Range
Character range; case-sensitive
/[a-z]/.test('m') // true
\b
Word boundary
Position between \w and \W; zero-width
/\bword\b/.test('a word') // true
\B
Non-word boundary
Opposite of \b
/\Bword/.test('sword') // true
^
Start of string/line
String start, or line start with /m flag
/^hello/.test('hello') // true
$
End of string/line
String end, or line end with /m flag
/world$/.test('world') // true
Quantifier
Matches
Description
Example
*
0 or more
Greedy; matches as many as possible
/a*/.test('') // true
+
1 or more
Greedy; at least one required
/a+/.test('aaa') // true
?
0 or 1
Optional; makes preceding element optional
/colou?r/.test('color') // true
{n}
Exactly n
Precise count
/\d{3}/.test('123') // true
{n,}
n or more
At least n
/\d{2,}/.test('12') // true
{n,m}
Between n and m
Inclusive range
/\d{2,4}/.test('123') // true
*?
0+ (lazy)
Non-greedy; matches as few as possible
/<.*?>/.exec('<a><b>')[0] // "<a>"
+?
1+ (lazy)
Non-greedy; at least one
/a+?/.exec('aaa')[0] // "a"
??
0-1 (lazy)
Non-greedy optional
/a??/.exec('aa')[0] // ""
{n,m}?
n-m (lazy)
Non-greedy range
/\d{2,4}?/.exec('12345')[0] // "12"
Example: Character classes and quantifiers
// Basic character classes
/\d+/.test('123'); // true (one or more digits)
/\D+/.test('abc'); // true (one or more non-digits)
/\w+/.test('hello_123'); // true (word characters)
/\s+/.test(' '); // true (whitespace)
// Dot (any character except newline)
/.+/.test('hello'); // true
/.+/.test('hello\nworld'); // false (. doesn't match \n)
/.+/s.test('hello\nworld'); // true (dotAll flag)
// Character sets
/[aeiou]/.test('hello'); // true (has vowel)
/[0-9]/.test('5'); // true (digit)
/[a-z]/i.test('A'); // true (case insensitive)
/[a-zA-Z0-9]/.test('x'); // true (alphanumeric)
// Negated sets
/[^0-9]/.test('a'); // true (not a digit)
/[^aeiou]/.test('b'); // true (not a vowel)
// Ranges
/[a-z]/.test('m'); // true
/[A-Z]/.test('M'); // true
/[0-9]/.test('5'); // true
/[a-zA-Z]/.test('X'); // true
// Word boundaries
/\bhello\b/.test('hello world'); // true
/\bhello\b/.test('helloworld'); // false
/\bworld\b/.test('hello world'); // true
// Extract whole words
'hello world'.match(/\b\w+\b/g); // ["hello", "world"]
// Start/End anchors
/^hello/.test('hello world'); // true
/^hello/.test('say hello'); // false
/world$/.test('hello world'); // true
/world$/.test('world peace'); // false
// Full string match
/^hello$/.test('hello'); // true
/^hello$/.test('hello world'); // false
// Greedy quantifiers (default)
'<a><b><c>'.match(/<.+>/)[0]; // "<a><b><c>" (greedy)
'aaaa'.match(/a+/)[0]; // "aaaa" (all)
// Non-greedy (lazy) quantifiers
'<a><b><c>'.match(/<.+?>/g); // ["<a>", "<b>", "<c>"]
'aaaa'.match(/a+?/)[0]; // "a" (minimal)
// Practical examples
// 1. Email (simple)
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test('user@example.com'); // true
// 2. Phone number
/^\d{3}-\d{3}-\d{4}$/.test('555-123-4567'); // true
// 3. Hex color
/^#[0-9A-F]{6}$/i.test('#FF5733'); // true
// 4. URL (simple)
/^https?:\/\/[\w.-]+/.test('https://example.com'); // true
// 5. Username (alphanumeric, 3-16 chars)
/^\w{3,16}$/.test('user_123'); // true
// 6. Password (at least 8 chars, has digit)
/^(?=.*\d).{8,}$/.test('Pass123!'); // true
// 7. Extract hashtags
'#javascript #coding #webdev'.match(/#\w+/g);
// ["#javascript", "#coding", "#webdev"]
// 8. Match HTML tags (non-greedy)
'<div>Hello</div>'.match(/<[^>]+>/g);
// ["<div>", "</div>"]
// 9. Trim whitespace
' hello '.replace(/^\s+|\s+$/g, ''); // "hello"
// 10. Multiple spaces to single
'hello world'.replace(/\s+/g, ' '); // "hello world"
// Combining patterns
const urlRe = /^(https?:\/\/)?([\w.-]+)(:\d+)?(\/[\w.-]*)*\??/;
// Optional elements with ?
/colou?r/.test('color'); // true (US)
/colou?r/.test('colour'); // true (UK)
// Exact count
/^\d{5}$/.test('12345'); // true (5 digits)
/^\d{3}-\d{2}-\d{4}$/.test('123-45-6789'); // true (SSN format)
// Range
/^[a-z]{3,10}$/i.test('hello'); // true (3-10 letters)
Warning: Greedy quantifiers (+, *) can cause catastrophic
backtracking with complex patterns. Use lazy quantifiers (+?, *?) or atomic groups
when appropriate.
11.6 Lookahead and Lookbehind Assertions
Assertion
Syntax
Description
Example
Positive Lookahead
(?=pattern)
Assert pattern matches ahead; zero-width (doesn't consume)
/\d(?=px)/.exec('5px')[0] // "5"
Negative Lookahead
(?!pattern)
Assert pattern does NOT match ahead; zero-width
/\d(?!px)/.test('5em') // true
Positive Lookbehind ES2018
(?<=pattern)
Assert pattern matches behind; zero-width
/(?<=\$)\d+/.exec('$50')[0] // "50"
Negative Lookbehind ES2018
(?<!pattern)
Assert pattern does NOT match behind; zero-width
/(?<!\$)\d+/.test('€50') // true
Example: Lookahead and lookbehind
// Positive lookahead (?=...)
// Match digit followed by "px" but don't include "px"
'5px 10em'.match(/\d+(?=px)/g); // ["5"]
// Password validation (at least one digit)
/^(?=.*\d).{8,}$/.test('Pass123!'); // true
/^(?=.*\d).{8,}$/.test('Password'); // false (no digit)
// Multiple lookaheads (AND conditions)
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;
strongPassword.test('Pass123!'); // true
strongPassword.test('password'); // false (no uppercase, digit, special)
// Negative lookahead (?!...)
// Match word NOT followed by "script"
'Java JavaScript'.match(/\bJava(?!Script)\b/); // ["Java"]
// Select numbers not followed by px
'5px 10em 15%'.match(/\d+(?!px)/g); // ["1", "0", "1", "5"]
// (matches last digit of 10, and 15)
// Better: not followed by px unit
'5px 10em 15%'.match(/\d+(?!px\b)/g); // ["10", "15"]
// Positive lookbehind (?<=...)
// Match digits preceded by $
'$50 €30 £40'.match(/(?<=\$)\d+/g); // ["50"]
// Extract value after key
'price=100 quantity=5'.match(/(?<==)\d+/g);
// ["100", "5"]
// Match word after "Mr. "
'Mr. Smith, Ms. Jones'.match(/(?<=Mr\. )\w+/);
// ["Smith"]
// Negative lookbehind (?<!...)
// Match digits NOT preceded by $
'$50 €30 £40'.match(/(?<!\$)\d+/g); // ["30", "40"]
// Word not preceded by "no "
'no cats, dogs, no birds'.match(/(?<!no )\b\w+s\b/g);
// ["dogs"] (cats and birds are preceded by "no ")
// Complex combinations
// 1. Price in $ (with decimal)
const priceRe = /(?<=\$)\d+(?:\.\d{2})?(?=\s|$)/g;
'Items: $10.50 and $25.00'.match(priceRe);
// ["10.50", "25.00"]
// 2. Password: 8+ chars, has lowercase, uppercase, digit
const passRe = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
passRe.test('Secure123'); // true
passRe.test('secure123'); // false (no uppercase)
passRe.test('SECURE123'); // false (no lowercase)
passRe.test('SecurePass'); // false (no digit)
// 3. Extract hashtags not in code blocks
// (not preceded by backtick)
const text = 'Use #javascript but not `#code`';
text.match(/(?<!`[^`]*)#\w+(?![^`]*`)/g); // ["#javascript"]
// 4. Numbers with thousands separator
const numRe = /\b(?<!\d,)\d{1,3}(?:,\d{3})*(?!\d)/g;
'1,000 and 10,000 and 999'.match(numRe);
// ["1,000", "10,000", "999"]
// 5. Email username (before @)
const emailRe = /\w+(?=@)/;
'user@example.com'.match(emailRe)[0]; // "user"
// 6. URL domain (after //)
const domainRe = /(?<=:\/\/)[^\/]+/;
'https://example.com/path'.match(domainRe)[0];
// "example.com"
// 7. Word boundaries with punctuation
// Word not followed by alphanumeric
'hello, world!'.match(/\w+(?!\w)/g);
// ["hello", "world"]
// 8. CSS selector (class not in comments)
const css = '.btn { } /* .comment */';
css.match(/(?<!\/\*.*)\.[a-z-]+(?!.*\*\/)/g);
// Tricky - better parsed differently
// Zero-width assertion chaining
// Number between 1-100
/^(?=\d{1,3}$)(?!0$)([1-9]\d?|100)$/.test('50'); // true
/^(?=\d{1,3}$)(?!0$)([1-9]\d?|100)$/.test('0'); // false
/^(?=\d{1,3}$)(?!0$)([1-9]\d?|100)$/.test('101'); // false
// Assertions don't consume characters
const re = /(?=\d{3})\d{2}/;
re.exec('12345'); // ["12"] (matched at position 0)
// Lookahead checked 3 digits, but only consumed 2
// Use in replace
// Insert comma in numbers
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// "1,234,567"
Note: Assertions are zero-width (don't consume characters).
Lookaheads work everywhere; lookbehinds require ES2018+. Use for complex validations and extractions.
11.7 Named Capture Groups and Replacement
Feature
Syntax
Description
Example
Named Group
(?<name>pattern)
Capture group with identifier; access via .groups property
/(?<year>\d{4})/
Group Reference
\k<name>
Backreference to named group within pattern
/(?<tag>\w+)</\k<tag>/
Replacement
$<name>
Reference named group in replacement string
.replace(re, '$<name>')
Non-capturing Group
(?:pattern)
Group without capturing; for grouping only; better performance
/(?:abc)+/
Numbered Group
(pattern)
Traditional capture group; access via index
/(\d+)/
Backreference
\1 \2 ...
Reference numbered group within pattern
/(\w)\1/ (double char)
Example: Named groups and backreferences
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;month>/
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;day>/
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '(
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;area>)
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;exchange>-
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;last>
// Named capture groups
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2025-12-17'.match(dateRe);
match[1]; // "2025" (by index)
match.groups.year; // "2025" (by name)
match.groups.month; // "12"
match.groups.day; // "17"
// Destructuring with named groups
const {groups: {year, month, day}} = '2025-12-17'.match(dateRe);
console.log(year, month, day); // "2025" "12" "17"
// Replace with named groups
const date = '2025-12-17';
date.replace(dateRe, '$<month>/$<day>/$<year>');
// "12/17/2025"
// Function replacement with named groups
date.replace(dateRe, (match, y, m, d, offset, str, groups) => {
return `${groups.month}/${groups.day}/${groups.year}`;
});
// Backreference to named group
const htmlTag = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
htmlTag.test('<div>content</div>'); // true
htmlTag.test('<div>content</span>'); // false (tag mismatch)
// Find duplicate words
const duplicateRe = /\b(?<word>\w+)\s+\k<word>\b/i;
duplicateRe.test('the the'); // true
duplicateRe.test('the cat'); // false
// Email parsing
const emailRe = /^(?<user>[^\s@]+)@(?<domain>[^\s@]+)\.(?<tld>\w+)$/;
const email = 'user@example.com'.match(emailRe);
email.groups.user; // "user"
email.groups.domain; // "example"
email.groups.tld; // "com"
// URL parsing
const urlRe = /^(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?$/;
const url = 'https://example.com/path/to/page'.match(urlRe);
url.groups.protocol; // "https"
url.groups.domain; // "example.com"
url.groups.path; // "/path/to/page"
// Phone number formatting
const phoneRe = /(?<area>\d{3})(?<exchange>\d{3})(?<number>\d{4})/;
'5551234567'.replace(phoneRe, '($<area>) $<exchange>-$<number>');
// "(555) 123-4567"
// Non-capturing groups (?:...)
// Group without capturing (performance)
/(?:abc)+/.test('abcabc'); // true
'abcabc'.match(/(?:abc)+/)[0]; // "abcabc"
'abcabc'.match(/(?:abc)+/).length; // 1 (no capture groups)
// Compare to capturing
'abcabc'.match(/(abc)+/); // ["abcabc", "abc"] (has capture)
// Use for alternation
/(?:cat|dog)s?/.test('cats'); // true (s is optional for whole group)
// Numbered backreferences
// Find doubled characters
/(\w)\1/.test('hello'); // true (ll)
'hello'.match(/(\w)\1/)[0]; // "ll"
// Find repeated words
/(\b\w+\b)\s+\1/.test('the the'); // true
// Match balanced quotes
/'([^']*)'/.exec("'hello'")[1]; // "hello"
// HTML attributes
/(\w+)="([^"]*)"/.exec('class="btn"');
// ["class=\"btn\"", "class", "btn"]
// Combining named and numbered groups
const re = /(?<name>\w+):(\d+)/;
const m = 'user:123'.match(re);
m.groups.name; // "user"
m[2]; // "123" (numbered after named)
// Replace with function (named groups)
'John Smith, Jane Doe'.replace(
/(?<first>\w+) (?<last>\w+)/g,
(match, first, last, offset, string, groups) => {
return `${groups.last}, ${groups.first}`;
}
);
// "Smith, John, Doe, Jane"
// Case conversion in replacement
'user_name'.replace(
/_(?<char>\w)/g,
(match, char, offset, string, groups) => {
return groups.char.toUpperCase();
}
);
// "userName"
// Swap parts
'firstName lastName'.replace(
/(?<first>\w+) (?<last>\w+)/,
'$<last> $<first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
#x3C;first>'
);
// "lastName firstName"
// Optional named groups
const optionalRe = /(?<protocol>https?:\/\/)?(?<domain>[^\/]+)/;
'example.com'.match(optionalRe).groups;
// {protocol: undefined, domain: "example.com"}
'https://example.com'.match(optionalRe).groups;
// {protocol: "https://", domain: "example.com"}
// matchAll with named groups
const text = 'user:123 admin:456';
const matches = text.matchAll(/(?<role>\w+):(?<id>\d+)/g);
for (const match of matches) {
console.log(match.groups.role, match.groups.id);
// "user" "123"
// "admin" "456"
}
Note: Named groups improve readability and maintainability. Use (?:...) for
grouping without capturing (better performance). Backreferences \k<name> or \1
match same text as group.
Section 11 Summary
- Creation: Literal
/pattern/flags for static (compile-time);
constructor new RegExp(str, flags) for dynamic (escape backslashes: '\\d')
- Flags:
g (global), i (case-insensitive),
m (multiline ^$), s (dotAll), u (Unicode), y (sticky),
d (indices); g makes regex stateful
- Methods:
test() boolean; exec() details + loop
with g; match() simple/all; matchAll() iterator with details (requires g)
- String methods:
replace/replaceAll modify; split
divide (includes captures); search find index; use $1 $<name> in replacement
- Patterns:
\d \w \s classes; . [abc] [^abc] [a-z]
sets; ^ $ \b anchors; + * ? {n,m} quantifiers; add ? for lazy
- Assertions:
(?=...) (?!...) lookahead;
(?<=...) (?<!...) lookbehind; zero-width (don't consume); chain for complex validation
- Groups:
(?<name>...) named capture; (?:...)
non-capturing; \k<name> \1 backreferences; $<name> $1 in replacements
12. Numbers, Math, and BigInt Operations
12.1 Number Types and Numeric Literals
| Type/Literal | Syntax | Description | Example |
|---|---|---|---|
| Decimal Integer | 123 |
Base 10 integer; most common form | 42, -17, 0 |
| Decimal Float | 1.23 |
Floating point number; IEEE 754 double precision (64-bit) | 3.14, -0.5, 0.1 |
| Scientific Notation | 1e10 |
Exponent form: number × 10^exponent | 1e6 (1000000), 5e-3 (0.005) |
| Binary ES2015 | 0b101 |
Base 2; prefix 0b or 0B; digits 0-1 | 0b1010 (10), 0b1111 (15) |
| Octal ES2015 | 0o17 |
Base 8; prefix 0o or 0O; digits 0-7 | 0o10 (8), 0o77 (63) |
| Hexadecimal | 0xFF |
Base 16; prefix 0x or 0X; digits 0-9, A-F | 0xFF (255), 0x1A (26) |
| Numeric Separators ES2021 | 1_000_000 |
Underscores for readability; ignored by engine | 1_000, 0xFF_FF, 0b1010_1010 |
| Special Values | Infinity, -Infinity, NaN |
Special numeric values; NaN ≠ NaN | 1/0, -1/0, 0/0 |
| Signed Zero | +0, -0 |
IEEE 754 has both; usually treated as equal | -0 === +0 // true |
Example: Numeric literal forms
// Decimal integers
const dec = 42;
const negative = -17;
const zero = 0;
// Decimal floats
const pi = 3.14159;
const small = 0.0001;
const leading = .5; // 0.5 (avoid - use 0.5)
const trailing = 5.; // 5 (avoid - use 5.0 or 5)
// Scientific notation
const million = 1e6; // 1000000
const billion = 1e9; // 1000000000
const tiny = 5e-3; // 0.005
const planck = 6.62607015e-34;
// Binary (base 2)
const binary = 0b1010; // 10
const flags = 0b11111111; // 255
const mask = 0b1100; // 12
// Octal (base 8)
const octal = 0o17; // 15
const permissions = 0o755; // 493 (rwxr-xr-x)
// Hexadecimal (base 16)
const hex = 0xFF; // 255
const color = 0xFF5733; // 16734003
const byte = 0xAB; // 171
// Mixed case OK
const HEX = 0XFF; // 255
const BIN = 0B1010; // 10
// Numeric separators (readability)
const oneMillion = 1_000_000;
const billion = 1_000_000_000;
const hexColor = 0xFF_55_33;
const binary8bit = 0b1111_0000;
const creditCard = 1234_5678_9012_3456;
// Separators anywhere except start/end
const amount = 123_456.789_012;
// Special values
const inf = Infinity;
const negInf = -Infinity;
const notANumber = NaN;
1 / 0; // Infinity
-1 / 0; // -Infinity
0 / 0; // NaN
Math.sqrt(-1); // NaN
// NaN is unique
NaN === NaN; // false ❌
isNaN(NaN); // true ✓
Number.isNaN(NaN); // true ✓ (preferred)
// Signed zeros
+0 === -0; // true
Object.is(+0, -0); // false (can distinguish)
1 / +0; // Infinity
1 / -0; // -Infinity
// Type checking
typeof 42; // "number"
typeof 3.14; // "number"
typeof NaN; // "number" (yes, really!)
typeof Infinity; // "number"
// Number range
Number.MAX_VALUE; // ~1.79e308
Number.MIN_VALUE; // ~5e-324 (smallest positive)
Number.MAX_SAFE_INTEGER; // 2^53 - 1 (9007199254740991)
Number.MIN_SAFE_INTEGER; // -(2^53 - 1)
Note: JavaScript numbers are 64-bit IEEE 754 floats. Use numeric
separators for readability. Prefer
Number.isNaN() over isNaN(). Safe integer
range: ±2^53-1.
12.2 Number Methods and Constants
| Method/Property | Syntax | Description | Returns |
|---|---|---|---|
| isFinite() | Number.isFinite(value) |
Check if finite number (not Infinity/NaN); no coercion | Boolean |
| isInteger() | Number.isInteger(value) |
Check if integer (no fractional part) | Boolean |
| isNaN() | Number.isNaN(value) |
Check if NaN; no coercion (safer than global isNaN()) | Boolean |
| isSafeInteger() | Number.isSafeInteger(value) |
Check if integer in safe range [-(2^53-1), 2^53-1] | Boolean |
| toExponential() | num.toExponential(digits) |
Scientific notation string; optional decimal digits | String |
| toFixed() | num.toFixed(digits) |
Fixed decimal places; rounds; 0-20 digits | String |
| toPrecision() | num.toPrecision(precision) |
Specified total significant digits; 1-21 | String |
| toString() | num.toString(radix) |
String representation; optional base (2-36) | String |
| valueOf() | num.valueOf() |
Primitive value; rarely needed | Number |
| toLocaleString() | num.toLocaleString(locale, options) |
Locale-aware formatting; currency, percentages, units | String |
| Constant | Value | Description | Use Case |
|---|---|---|---|
| EPSILON | ~2.22e-16 | Smallest difference between 1 and next representable number | Float comparison tolerance |
| MAX_VALUE | ~1.79e308 | Largest positive number | Range checking |
| MIN_VALUE | ~5e-324 | Smallest positive number (near zero) | Precision limits |
| MAX_SAFE_INTEGER | 9007199254740991 | 2^53 - 1; largest safe integer | Integer arithmetic bounds |
| MIN_SAFE_INTEGER | -9007199254740991 | -(2^53 - 1); smallest safe integer | Integer arithmetic bounds |
| POSITIVE_INFINITY | Infinity | Positive infinity; same as global Infinity | Overflow detection |
| NEGATIVE_INFINITY | -Infinity | Negative infinity | Underflow detection |
| NaN | NaN | Not-a-Number; same as global NaN | Invalid operations |
Example: Number methods and constants
// Type checking methods
Number.isFinite(42); // true
Number.isFinite(Infinity); // false
Number.isFinite(NaN); // false
Number.isFinite('42'); // false (no coercion!)
// Compare: global isFinite() coerces
isFinite('42'); // true (coerces to 42)
Number.isFinite('42'); // false (no coercion)
Number.isInteger(42); // true
Number.isInteger(42.0); // true (same as 42)
Number.isInteger(42.5); // false
Number.isInteger('42'); // false
Number.isNaN(NaN); // true
Number.isNaN(42); // false
Number.isNaN('hello'); // false (no coercion!)
// Compare: global isNaN() coerces
isNaN('hello'); // true (coerces to NaN)
Number.isNaN('hello'); // false (no coercion)
Number.isSafeInteger(42); // true
Number.isSafeInteger(9007199254740991); // true (MAX_SAFE_INTEGER)
Number.isSafeInteger(9007199254740992); // false (too large)
Number.isSafeInteger(42.5); // false (not integer)
// Formatting methods
const num = 123.456;
num.toFixed(2); // "123.46" (rounds)
num.toFixed(0); // "123"
num.toFixed(5); // "123.45600"
num.toExponential(2); // "1.23e+2"
(1234.5).toExponential(); // "1.2345e+3"
num.toPrecision(4); // "123.5"
num.toPrecision(2); // "1.2e+2" (switches to exponential)
num.toPrecision(10); // "123.4560000"
// Base conversion
(255).toString(); // "255"
(255).toString(16); // "ff" (hex)
(255).toString(2); // "11111111" (binary)
(255).toString(8); // "377" (octal)
(255).toString(36); // "73" (max base)
// Locale formatting
const price = 1234.56;
price.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
}); // "$1,234.56"
price.toLocaleString('de-DE', {
style: 'currency',
currency: 'EUR'
}); // "1.234,56 €"
const percent = 0.755;
percent.toLocaleString('en-US', {
style: 'percent',
minimumFractionDigits: 1
}); // "75.5%"
// Constants
Number.EPSILON; // 2.220446049250313e-16
Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MIN_SAFE_INTEGER; // -9007199254740991
// Safe integer range check
function isSafeArithmetic(a, b) {
const result = a + b;
return Number.isSafeInteger(a) &&
Number.isSafeInteger(b) &&
Number.isSafeInteger(result);
}
// Floating point comparison with EPSILON
function almostEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
almostEqual(0.1 + 0.2, 0.3); // true
0.1 + 0.2 === 0.3; // false ❌
// Format large numbers
const large = 1234567890;
large.toLocaleString('en-US'); // "1,234,567,890"
// Scientific notation
(0.0000001).toExponential(); // "1e-7"
(10000000).toExponential(); // "1e+7"
// Precision control
const value = 123.456789;
value.toFixed(2); // "123.46"
value.toPrecision(5); // "123.46"
parseFloat(value.toFixed(2)); // 123.46 (back to number)
Warning:
toFixed(), toExponential(), toPrecision()
return strings. Use Number.is*() over global versions (no coercion).
Safe integers: ±(2^53-1).
12.3 Math Object Methods and Constants
| Method | Description | Example | Result |
|---|---|---|---|
| abs(x) | Absolute value | Math.abs(-5) |
5 |
| ceil(x) | Round up to integer | Math.ceil(4.1) |
5 |
| floor(x) | Round down to integer | Math.floor(4.9) |
4 |
| round(x) | Round to nearest integer | Math.round(4.5) |
5 |
| trunc(x) | Remove fractional part | Math.trunc(4.9) |
4 |
| sign(x) | Sign: -1, 0, or 1 | Math.sign(-5) |
-1 |
| min(...args) | Smallest of arguments | Math.min(2, 5, 1) |
1 |
| max(...args) | Largest of arguments | Math.max(2, 5, 1) |
5 |
| pow(x, y) | x to power y; use ** operator | Math.pow(2, 3) |
8 |
| sqrt(x) | Square root | Math.sqrt(16) |
4 |
| cbrt(x) | Cube root | Math.cbrt(27) |
3 |
| exp(x) | e^x (Euler's number) | Math.exp(1) |
~2.718 |
| log(x) | Natural logarithm (base e) | Math.log(Math.E) |
1 |
| log10(x) | Base 10 logarithm | Math.log10(100) |
2 |
| log2(x) | Base 2 logarithm | Math.log2(8) |
3 |
| random() | Random [0, 1); pseudo-random | Math.random() |
0.123... |
| Trigonometry | Description | Hyperbolic | Description |
|---|---|---|---|
| sin(x) | Sine (radians) | sinh(x) | Hyperbolic sine |
| cos(x) | Cosine (radians) | cosh(x) | Hyperbolic cosine |
| tan(x) | Tangent (radians) | tanh(x) | Hyperbolic tangent |
| asin(x) | Arcsine | asinh(x) | Inverse hyperbolic sine |
| acos(x) | Arccosine | acosh(x) | Inverse hyperbolic cosine |
| atan(x) | Arctangent | atanh(x) | Inverse hyperbolic tangent |
| atan2(y, x) | Angle from origin to (x,y) | hypot(...args) | √(x²+y²+...) - Euclidean norm |
| Constant | Value | Description | Formula |
|---|---|---|---|
| E | ~2.718 | Euler's number | Base of natural logarithms |
| PI | ~3.14159 | Pi (π) | Circle circumference / diameter |
| LN2 | ~0.693 | ln(2) | Natural log of 2 |
| LN10 | ~2.303 | ln(10) | Natural log of 10 |
| LOG2E | ~1.443 | log₂(e) | Base 2 log of e |
| LOG10E | ~0.434 | log₁₀(e) | Base 10 log of e |
| SQRT1_2 | ~0.707 | √(1/2) | Square root of 0.5 |
| SQRT2 | ~1.414 | √2 | Square root of 2 |
Example: Math operations
// Rounding
Math.round(4.5); // 5
Math.round(4.4); // 4
Math.round(-4.5); // -4 (rounds to nearest even)
Math.ceil(4.1); // 5 (always up)
Math.floor(4.9); // 4 (always down)
Math.trunc(4.9); // 4 (remove decimal)
// Difference: floor vs trunc with negatives
Math.floor(-4.1); // -5 (down = more negative)
Math.trunc(-4.1); // -4 (just removes decimal)
// Absolute value and sign
Math.abs(-5); // 5
Math.abs(5); // 5
Math.sign(-5); // -1
Math.sign(0); // 0
Math.sign(5); // 1
// Min/Max
Math.min(5, 2, 8, 1); // 1
Math.max(5, 2, 8, 1); // 8
// With arrays (spread)
const nums = [5, 2, 8, 1];
Math.min(...nums); // 1
Math.max(...nums); // 8
// Power and roots
Math.pow(2, 3); // 8
2 ** 3; // 8 (exponentiation operator - preferred)
Math.sqrt(16); // 4
Math.sqrt(2); // 1.4142135623730951
Math.cbrt(27); // 3
Math.cbrt(8); // 2
// Logarithms
Math.log(Math.E); // 1 (natural log)
Math.log10(100); // 2
Math.log2(8); // 3
// Exponential
Math.exp(1); // 2.718281828459045 (e^1)
Math.exp(2); // 7.389... (e^2)
// Random numbers
Math.random(); // 0 <= x < 1
// Random integer in range [min, max]
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6); // dice roll (1-6)
// Random from array
function randomItem(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// Trigonometry (radians!)
Math.sin(Math.PI / 2); // 1
Math.cos(0); // 1
Math.tan(Math.PI / 4); // ~1
// Degrees to radians
function toRadians(degrees) {
return degrees * Math.PI / 180;
}
// Radians to degrees
function toDegrees(radians) {
return radians * 180 / Math.PI;
}
Math.sin(toRadians(30)); // 0.5
// atan2 - angle from origin to point
Math.atan2(1, 1); // π/4 (45 degrees)
Math.atan2(1, 0); // π/2 (90 degrees)
// Hypotenuse (Pythagorean)
Math.hypot(3, 4); // 5 (3² + 4² = 5²)
Math.hypot(1, 1); // √2 (1.414...)
// Distance between 2D points
function distance(x1, y1, x2, y2) {
return Math.hypot(x2 - x1, y2 - y1);
}
// Constants
Math.PI; // 3.141592653589793
Math.E; // 2.718281828459045
// Circle calculations
const radius = 5;
const circumference = 2 * Math.PI * radius; // 31.4...
const area = Math.PI * radius ** 2; // 78.5...
// Practical uses
// 1. Clamp value to range
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// 2. Linear interpolation
function lerp(start, end, t) {
return start + (end - start) * t;
}
// 3. Map value from one range to another
function map(value, inMin, inMax, outMin, outMax) {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
// 4. Round to decimal places
function roundTo(num, places) {
const factor = 10 ** places;
return Math.round(num * factor) / factor;
}
roundTo(3.14159, 2); // 3.14
Note: Math methods work with radians, not degrees. Use
** operator over
Math.pow(). Math.random() is pseudo-random (not cryptographically secure - use
crypto.getRandomValues() for security).
12.4 Number Precision and Floating Point Issues
| Issue | Cause | Example | Solution |
|---|---|---|---|
| Binary Representation | IEEE 754 binary cannot represent some decimals exactly | 0.1 + 0.2 !== 0.3 |
Use epsilon comparison or integer math |
| Rounding Errors | Accumulated precision loss in calculations | 0.1 + 0.2 = 0.30000000000000004 |
Round to fixed decimals, use libraries (decimal.js) |
| Large Integer Loss | Integers > 2^53-1 lose precision | 9007199254740992 + 1 === 9007199254740992 |
Use BigInt for large integers |
| Division by Zero | Returns Infinity (not error) | 1 / 0 = Infinity |
Check denominator before division |
| NaN Propagation | Any operation with NaN yields NaN | NaN + 5 = NaN |
Validate inputs, use Number.isNaN() |
| Epsilon Comparison | Float equality unreliable | 0.1 + 0.2 === 0.3 // false |
Math.abs(a - b) < Number.EPSILON |
Example: Floating point precision handling
// Classic floating point problem
0.1 + 0.2; // 0.30000000000000004 ❌
0.1 + 0.2 === 0.3; // false
// Epsilon comparison for floats
function almostEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) < epsilon;
}
almostEqual(0.1 + 0.2, 0.3); // true ✓
// For larger numbers, scale epsilon
function almostEqualScaled(a, b) {
const epsilon = Math.max(Math.abs(a), Math.abs(b)) * Number.EPSILON;
return Math.abs(a - b) <= epsilon;
}
// Round to fixed decimals
function roundTo(num, decimals) {
const factor = 10 ** decimals;
return Math.round(num * factor) / factor;
}
roundTo(0.1 + 0.2, 1); // 0.3 ✓
roundTo(1.005, 2); // 1.01 (beware banker's rounding)
// Integer math for currency (cents)
// ❌ Don't do this:
const price = 0.1 + 0.2; // 0.30000000000000004
// ✓ Do this:
const priceInCents = 10 + 20; // 30
const price = priceInCents / 100; // 0.3
// Financial calculations
function addCurrency(a, b) {
return (a * 100 + b * 100) / 100;
}
addCurrency(0.1, 0.2); // 0.3 ✓
// Large integer precision loss
const maxSafe = Number.MAX_SAFE_INTEGER; // 9007199254740991
maxSafe + 1; // 9007199254740992 ✓
maxSafe + 2; // 9007199254740992 ❌ (lost precision!)
// Check safe integer range
Number.isSafeInteger(maxSafe); // true
Number.isSafeInteger(maxSafe + 1); // true
Number.isSafeInteger(maxSafe + 2); // false
// Division by zero
1 / 0; // Infinity (not error!)
-1 / 0; // -Infinity
0 / 0; // NaN
// Check for valid result
function safeDivide(a, b) {
if (b === 0) return null;
return a / b;
}
// NaN propagation
const result = NaN + 5; // NaN
Math.sqrt(-1) * 2; // NaN
// Always check NaN
function safeCalculation(a, b) {
const result = a / b;
if (Number.isNaN(result)) {
throw new Error('Invalid calculation');
}
return result;
}
// Rounding modes
Math.round(2.5); // 3
Math.round(-2.5); // -2 (rounds towards zero for negatives)
Math.floor(2.9); // 2
Math.floor(-2.1); // -3 (towards negative infinity)
Math.ceil(2.1); // 3
Math.ceil(-2.9); // -2 (towards positive infinity)
Math.trunc(2.9); // 2
Math.trunc(-2.9); // -2 (just removes decimal)
// Banker's rounding (round half to even)
function bankersRound(num, decimals = 0) {
const factor = 10 ** decimals;
const n = num * factor;
const floor = Math.floor(n);
const diff = n - floor;
if (diff === 0.5) {
return floor % 2 === 0 ? floor / factor : Math.ceil(n) / factor;
}
return Math.round(n) / factor;
}
// Comparing with tolerance
function withinTolerance(a, b, tolerance = 0.001) {
return Math.abs(a - b) <= tolerance;
}
withinTolerance(0.1 + 0.2, 0.3, 0.001); // true
// Use decimal libraries for precision
// npm install decimal.js
// const Decimal = require('decimal.js');
// new Decimal(0.1).plus(0.2).equals(0.3); // true
Warning: Never use
=== for float comparison. Use epsilon tolerance. For currency,
work in cents (integers). Large integers require BigInt. IEEE 754 has inherent precision limits.
12.5 BigInt Creation and Arithmetic Operations
| Feature | Syntax | Description | Example |
|---|---|---|---|
| Literal ES2020 | 123n |
Arbitrary precision integers; append n suffix | 9007199254740993n |
| Constructor | BigInt(value) |
Convert number/string to BigInt | BigInt("999") |
| Addition | a + b |
Both operands must be BigInt | 10n + 20n = 30n |
| Subtraction | a - b |
Both operands must be BigInt | 50n - 20n = 30n |
| Multiplication | a * b |
Both operands must be BigInt | 10n * 20n = 200n |
| Division | a / b |
Integer division (rounds towards zero) | 10n / 3n = 3n |
| Remainder | a % b |
Modulo operation | 10n % 3n = 1n |
| Exponentiation | a ** b |
Power operation; exponent must be non-negative | 2n ** 100n |
| Comparison | < > <= >= == |
Works across BigInt and Number (loose ==) | 10n < 20 // true |
| Bitwise | & | ^ ~ << >> |
All bitwise operators supported | 5n & 3n = 1n |
| Type Check | typeof value |
Returns "bigint" | typeof 10n === "bigint" |
Example: BigInt usage
// Creation
const big1 = 9007199254740993n; // literal with n suffix
const big2 = BigInt(9007199254740993); // constructor
const big3 = BigInt("9007199254740993"); // from string (for very large)
// Cannot mix BigInt and Number
10n + 20; // ❌ TypeError: Cannot mix BigInt and other types
10n + 20n; // ✓ 30n
// Convert between types
const num = 42;
const big = BigInt(num); // Number to BigInt
const backToNum = Number(big); // BigInt to Number (may lose precision!)
// Arithmetic operations
100n + 50n; // 150n
100n - 50n; // 50n
10n * 20n; // 200n
// Division truncates (integer division)
10n / 3n; // 3n (not 3.333...)
-10n / 3n; // -3n (rounds towards zero)
10n % 3n; // 1n (remainder)
// Exponentiation
2n ** 100n; // huge number
2n ** -1n; // ❌ RangeError: exponent must be non-negative
// Comparison
10n < 20n; // true
10n === 10n; // true
10n === 10; // false (strict equality - different types)
10n == 10; // true (loose equality - coerces)
// Mixed comparisons (non-equality)
10n < 20; // true ✓
10n > 5; // true ✓
20n <= 20; // true ✓
// Bitwise operations
const a = 5n; // 0101
const b = 3n; // 0011
a & b; // 1n (0001)
a | b; // 7n (0111)
a ^ b; // 6n (0110)
~a; // -6n (bitwise NOT)
5n << 2n; // 20n (shift left)
20n >> 2n; // 5n (shift right)
// No unsigned right shift (>>>) for BigInt
// Large factorials (no overflow!)
function factorial(n) {
let result = 1n;
for (let i = 2n; i <= n; i++) {
result *= i;
}
return result;
}
factorial(100n); // huge number (no precision loss!)
// Compare with regular number (loses precision)
function factorialNumber(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
factorialNumber(100); // Infinity (overflow!)
// Large integer operations
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
maxSafe + 1n; // works correctly!
maxSafe + 2n; // still correct!
// Crypto/hashing with BigInt
const large = 2n ** 256n; // 256-bit number
// No fractional BigInt
const frac = 1.5n; // ❌ SyntaxError
const fromFloat = BigInt(1.5); // ❌ RangeError
// Must be integer
const safe = BigInt(Math.floor(1.5)); // ✓ 1n
// Type checking
typeof 10n; // "bigint"
10n instanceof BigInt; // ❌ false (primitives not instances)
// Conversion gotchas
Number(2n ** 100n); // Infinity (too large for Number!)
// Safe conversion check
function safeToNumber(bigint) {
const num = Number(bigint);
if (!Number.isSafeInteger(num)) {
throw new Error('BigInt too large for safe Number conversion');
}
return num;
}
// JSON doesn't support BigInt
JSON.stringify({value: 10n}); // ❌ TypeError
// Custom JSON serialization
JSON.stringify({value: 10n}, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
); // '{"value":"10"}'
// Math methods don't work with BigInt
Math.max(10n, 20n); // ❌ TypeError
Math.sqrt(100n); // ❌ TypeError
// Must use BigInt operations
function maxBigInt(...args) {
return args.reduce((max, val) => val > max ? val : max);
}
maxBigInt(10n, 50n, 30n); // 50n
// Practical: microsecond timestamps
const timestamp = BigInt(Date.now()) * 1000n; // microseconds
// Practical: precise money calculations
const cents = 12345n; // $123.45
const dollars = Number(cents) / 100; // 123.45
Warning: Cannot mix BigInt with Number in operations. Division truncates. No fractional BigInt.
JSON doesn't support BigInt. Math methods incompatible with BigInt.
12.6 Number Parsing and Validation
| Function | Syntax | Description | Returns |
|---|---|---|---|
| parseInt() | parseInt(string, radix) |
Parse string to integer; optional base (2-36); stops at non-digit | Integer or NaN |
| parseFloat() | parseFloat(string) |
Parse string to float; stops at invalid character | Float or NaN |
| Number() | Number(value) |
Convert to number; strict (entire string must be valid) | Number or NaN |
| Unary Plus | +value |
Shorthand for Number(); strict conversion | Number or NaN |
| isNaN() | isNaN(value) |
Global; coerces to number first (avoid) | Boolean |
| Number.isNaN() | Number.isNaN(value) |
Strict; true only if value is NaN (preferred) | Boolean |
| isFinite() | isFinite(value) |
Global; coerces to number first | Boolean |
| Number.isFinite() | Number.isFinite(value) |
Strict; no coercion (preferred) | Boolean |
| Number.isInteger() | Number.isInteger(value) |
Check if integer (no fractional part) | Boolean |
| Number.isSafeInteger() | Number.isSafeInteger(value) |
Check if integer in safe range [-(2^53-1), 2^53-1] | Boolean |
Example: Parsing and validation
// parseInt - parses until invalid character
parseInt('123'); // 123
parseInt('123.45'); // 123 (stops at decimal)
parseInt('123px'); // 123 (stops at 'p')
parseInt('abc'); // NaN (no valid digits)
parseInt(' 42'); // 42 (trims whitespace)
// Always specify radix!
parseInt('08'); // 8 (may be octal in old browsers without radix!)
parseInt('08', 10); // 8 ✓ (always use radix)
// Different bases
parseInt('FF', 16); // 255 (hexadecimal)
parseInt('1010', 2); // 10 (binary)
parseInt('77', 8); // 63 (octal)
parseInt('Z', 36); // 35 (max base)
// Radix validation
parseInt('123', 10); // 123
parseInt('123', 2); // 1 (stops at '2', invalid for binary)
// parseFloat - decimal parsing
parseFloat('123.45'); // 123.45
parseFloat('123.45.67'); // 123.45 (stops at second '.')
parseFloat('123.45px'); // 123.45 (stops at 'p')
parseFloat('.5'); // 0.5
parseFloat('5.'); // 5
// Scientific notation
parseFloat('1.23e10'); // 12300000000
parseFloat('1e-5'); // 0.00001
// Number() - strict conversion
Number('123'); // 123
Number('123.45'); // 123.45
Number('123px'); // NaN ❌ (entire string must be valid)
Number(' 42 '); // 42 (trims whitespace)
Number(''); // 0 (empty string is 0!)
Number(' '); // 0 (whitespace only is 0)
// Unary plus (shorthand)
+'123'; // 123
+'123.45'; // 123.45
+'123px'; // NaN
// Comparison: parseInt vs parseFloat vs Number
const str = '123.45px';
parseInt(str); // 123 (stops at decimal)
parseFloat(str); // 123.45 (stops at 'p')
Number(str); // NaN (invalid)
// Type coercion gotchas
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
Number([]); // 0 (empty array)
Number([5]); // 5 (single element)
Number([1, 2]); // NaN (multiple elements)
// Validation: isNaN vs Number.isNaN
isNaN('hello'); // true (coerces 'hello' to NaN)
Number.isNaN('hello'); // false (string is not NaN)
Number.isNaN(NaN); // true ✓
isNaN(undefined); // true (coerces to NaN)
Number.isNaN(undefined); // false (undefined is not NaN)
// Rule: Use Number.isNaN() for strict checking
Number.isNaN(0 / 0); // true
Number.isNaN(parseFloat('abc')); // true
// Validation: isFinite vs Number.isFinite
isFinite('123'); // true (coerces to 123)
Number.isFinite('123'); // false (string is not finite number)
isFinite(Infinity); // false
Number.isFinite(Infinity); // false
isFinite(NaN); // false
Number.isFinite(NaN); // false
// Rule: Use Number.isFinite() for strict checking
// Integer validation
Number.isInteger(42); // true
Number.isInteger(42.0); // true (same as 42)
Number.isInteger(42.5); // false
Number.isInteger('42'); // false (no coercion)
// Safe integer validation
Number.isSafeInteger(42); // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1); // false
Number.isSafeInteger(42.0); // true
// Practical validation functions
function isValidNumber(value) {
return typeof value === 'number' &&
Number.isFinite(value) &&
!Number.isNaN(value);
}
function parseOrDefault(str, defaultValue = 0) {
const num = parseFloat(str);
return Number.isNaN(num) ? defaultValue : num;
}
function strictParse(str) {
const num = Number(str);
if (Number.isNaN(num)) {
throw new Error(`Invalid number: ${str}`);
}
return num;
}
// Safe parsing
function safeParse(str) {
const trimmed = str.trim();
if (trimmed === '') return null;
const num = Number(trimmed);
return Number.isNaN(num) ? null : num;
}
safeParse('123'); // 123
safeParse('abc'); // null
safeParse(''); // null
// Parsing with validation
function parsePositiveInt(str) {
const num = parseInt(str, 10);
if (Number.isNaN(num) || num <= 0 || !Number.isInteger(num)) {
return null;
}
return num;
}
// Currency parsing
function parseCurrency(str) {
// Remove currency symbols and commas
const cleaned = str.replace(/[$,]/g, '');
const num = parseFloat(cleaned);
return Number.isNaN(num) ? null : num;
}
parseCurrency('$1,234.56'); // 1234.56
parseCurrency('€1.234,56'); // requires more complex parsing
// Input validation
function validateInput(input) {
// Check if empty
if (!input || input.trim() === '') {
return {valid: false, error: 'Input required'};
}
// Try to parse
const num = Number(input);
// Check if valid number
if (Number.isNaN(num)) {
return {valid: false, error: 'Not a valid number'};
}
// Check if finite
if (!Number.isFinite(num)) {
return {valid: false, error: 'Must be finite'};
}
return {valid: true, value: num};
}
Note: Always specify radix for
parseInt(). Use Number.isNaN() and
Number.isFinite() (no coercion) over global versions. Number() strict (entire string),
parseInt/parseFloat lenient (stops at invalid).
Section 12 Summary
- Literals: Decimal, binary (0b), octal (0o), hex (0x), scientific (1e6); numeric separators (1_000_000) for readability; special values: Infinity, -Infinity, NaN
- Number methods:
isFinite/isInteger/isNaN/isSafeIntegervalidation (no coercion);toFixed/toExponential/toPrecisionformat (return strings);toString(radix)base conversion - Math object:
round/ceil/floor/truncrounding;min/max/abs/signbasic;pow/sqrt/cbrtpowers/roots;random()[0,1); trig functions use radians - Precision: IEEE 754 float issues (0.1+0.2≠0.3); use epsilon comparison
(
Math.abs(a-b)<Number.EPSILON); work in integers for currency; safe integer range: ±(2^53-1) - BigInt: Arbitrary precision integers with
nsuffix (123n); cannot mix with Number; division truncates; no fractional BigInt; use for >2^53-1 integers - Parsing:
parseInt(str, radix)lenient (stops at invalid);parseFloat(str)decimal;Number(value)strict (entire string); always use radix with parseInt - Validation: Prefer
Number.is*()over global versions (no coercion);isNaN/isFinite/isInteger/isSafeInteger; validate before arithmetic to prevent NaN propagation
13. Date and Time Handling
13.1 Date Object Creation and Methods
| Creation Method | Syntax | Description | Example |
|---|---|---|---|
| Current Date/Time | new Date() |
Current date and time (local timezone) | new Date() |
| From Timestamp | new Date(milliseconds) |
Milliseconds since Unix epoch (Jan 1, 1970 UTC) | new Date(0) |
| From String | new Date(dateString) |
Parse date string; format varies (prefer ISO 8601) | new Date('2024-01-15') |
| From Components | new Date(year, month, ...) |
Year, month (0-11), day (1-31), hour, min, sec, ms | new Date(2024, 0, 15) |
| Static Now | Date.now() |
Current timestamp in milliseconds | Date.now() |
| Copy Date | new Date(date) |
Create new Date from existing Date object | new Date(oldDate) |
| Getter Methods | Returns | UTC Equivalent | Description |
|---|---|---|---|
| getFullYear() | 4-digit year | getUTCFullYear() | 2024 (not 24!) |
| getMonth() | 0-11 | getUTCMonth() | 0=Jan, 11=Dec |
| getDate() | 1-31 | getUTCDate() | Day of month |
| getDay() | 0-6 | getUTCDay() | 0=Sunday, 6=Saturday |
| getHours() | 0-23 | getUTCHours() | Hour (24-hour format) |
| getMinutes() | 0-59 | getUTCMinutes() | Minutes |
| getSeconds() | 0-59 | getUTCSeconds() | Seconds |
| getMilliseconds() | 0-999 | getUTCMilliseconds() | Milliseconds |
| getTime() | Number | - | Timestamp (ms since epoch) |
| getTimezoneOffset() | Minutes | - | Offset from UTC (negative for ahead) |
| Setter Methods | Parameters | UTC Equivalent | Returns |
|---|---|---|---|
| setFullYear() | year, [month], [day] | setUTCFullYear() | Timestamp |
| setMonth() | month (0-11), [day] | setUTCMonth() | Timestamp |
| setDate() | day (1-31) | setUTCDate() | Timestamp |
| setHours() | hour, [min], [sec], [ms] | setUTCHours() | Timestamp |
| setMinutes() | min, [sec], [ms] | setUTCMinutes() | Timestamp |
| setSeconds() | sec, [ms] | setUTCSeconds() | Timestamp |
| setMilliseconds() | ms (0-999) | setUTCMilliseconds() | Timestamp |
| setTime() | timestamp | - | Timestamp |
Example: Date creation and methods
// Current date/time
const now = new Date();
console.log(now); // e.g., "2024-01-15T10:30:45.123Z"
// From timestamp (ms since Jan 1, 1970 UTC)
const epoch = new Date(0); // "1970-01-01T00:00:00.000Z"
const timestamp = new Date(1705318245123);
// Current timestamp (faster than new Date().getTime())
Date.now(); // 1705318245123
// From ISO string (recommended format)
const isoDate = new Date('2024-01-15T10:30:00.000Z'); // UTC
const isoLocal = new Date('2024-01-15T10:30:00'); // local time
const dateOnly = new Date('2024-01-15'); // midnight UTC
// ⚠️ Date string parsing is implementation-dependent!
new Date('Jan 15, 2024'); // works but inconsistent
new Date('15 Jan 2024'); // may fail in some browsers
new Date('2024/01/15'); // works
// From components (month is 0-indexed!)
const date = new Date(2024, 0, 15); // Jan 15, 2024 (local time)
const fullDate = new Date(2024, 0, 15, 10, 30, 45, 123);
// Year, Month(0-11), Day, Hour, Min, Sec, Ms
// ⚠️ Month is 0-indexed (0=Jan, 11=Dec)
new Date(2024, 0, 15); // January 15
new Date(2024, 11, 25); // December 25
// Copy date
const original = new Date();
const copy = new Date(original);
copy.setDate(copy.getDate() + 1); // doesn't affect original
// Getter methods (local time)
const d = new Date('2024-01-15T10:30:45.123');
d.getFullYear(); // 2024 (NOT getYear()!)
d.getMonth(); // 0 (January, 0-indexed!)
d.getDate(); // 15 (day of month, 1-31)
d.getDay(); // 1 (Monday, 0=Sunday)
d.getHours(); // 10
d.getMinutes(); // 30
d.getSeconds(); // 45
d.getMilliseconds(); // 123
d.getTime(); // 1705318245123 (timestamp)
// Timezone offset
d.getTimezoneOffset(); // e.g., -300 (minutes, UTC-5)
// Negative means ahead of UTC (e.g., UTC+5 = -300)
// Setter methods (chainable via return value)
const date = new Date('2024-01-15');
date.setFullYear(2025); // 2025-01-15
date.setMonth(11); // December (0-indexed!)
date.setDate(25); // 2025-12-25
date.setHours(10, 30, 0, 0); // 10:30:00.000
// Setters return timestamp
const timestamp = date.setDate(20); // returns new timestamp
// Out-of-range values auto-adjust
date.setDate(32); // rolls to next month
date.setMonth(12); // rolls to next year (Jan)
date.setDate(0); // goes to last day of previous month
const feb = new Date(2024, 1, 30); // Feb 30 → Mar 1
const endOfMonth = new Date(2024, 2, 0); // Mar 0 → Feb 29
// Invalid dates
const invalid = new Date('invalid');
invalid.toString(); // "Invalid Date"
isNaN(invalid.getTime()); // true
invalid.getTime(); // NaN
// Check validity
function isValidDate(d) {
return d instanceof Date && !isNaN(d.getTime());
}
// Compare dates
const d1 = new Date('2024-01-15');
const d2 = new Date('2024-01-20');
d1 < d2; // true
d1.getTime() < d2.getTime(); // true (more explicit)
// Equality requires .getTime()
d1 === d2; // false (different objects)
d1.getTime() === d2.getTime(); // false (different times)
const d3 = new Date('2024-01-15');
d1.getTime() === d3.getTime(); // true
// Date arithmetic (use timestamps)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
// Common patterns
// Start of day
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
// End of day
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
// Age calculation
function getAge(birthDate) {
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
Warning: Month is 0-indexed (0=Jan, 11=Dec) but day is 1-indexed. Date string parsing varies by
browser - use ISO 8601 format.
getYear() is deprecated - use getFullYear().
13.2 Date Formatting and Parsing
| Method | Format | Example Output | Notes |
|---|---|---|---|
| toString() | Full string | "Mon Jan 15 2024 10:30:45 GMT-0500" | Locale-dependent |
| toDateString() | Date only | "Mon Jan 15 2024" | Human-readable |
| toTimeString() | Time only | "10:30:45 GMT-0500 (EST)" | With timezone |
| toISOString() | ISO 8601 UTC | "2024-01-15T15:30:45.123Z" | Standard, UTC only |
| toJSON() | ISO 8601 UTC | "2024-01-15T15:30:45.123Z" | Same as toISOString() |
| toUTCString() | UTC string | "Mon, 15 Jan 2024 15:30:45 GMT" | RFC-1123 format |
| toLocaleString() | Locale format | "1/15/2024, 10:30:45 AM" | Locale-aware |
| toLocaleDateString() | Locale date | "1/15/2024" | Date only, locale |
| toLocaleTimeString() | Locale time | "10:30:45 AM" | Time only, locale |
| valueOf() | Timestamp | 1705318245123 | Milliseconds since epoch |
| Parsing Function | Input | Returns | Notes |
|---|---|---|---|
| Date.parse() | Date string | Timestamp (ms) or NaN | Inconsistent across browsers |
| new Date(string) | Date string | Date object | Uses Date.parse() internally |
| Date.UTC() | Year, month, day, ... | UTC timestamp | Like constructor but UTC |
Example: Date formatting and parsing
const date = new Date('2024-01-15T10:30:45.123Z');
// String representations
date.toString();
// "Mon Jan 15 2024 05:30:45 GMT-0500 (EST)"
date.toDateString();
// "Mon Jan 15 2024"
date.toTimeString();
// "05:30:45 GMT-0500 (Eastern Standard Time)"
date.toISOString();
// "2024-01-15T10:30:45.123Z" (always UTC, Z suffix)
date.toJSON();
// "2024-01-15T10:30:45.123Z" (same as toISOString)
date.toUTCString();
// "Mon, 15 Jan 2024 10:30:45 GMT" (RFC-1123)
// Locale-aware formatting
date.toLocaleString('en-US');
// "1/15/2024, 5:30:45 AM"
date.toLocaleDateString('en-US');
// "1/15/2024"
date.toLocaleTimeString('en-US');
// "5:30:45 AM"
// Different locales
date.toLocaleString('en-GB');
// "15/01/2024, 05:30:45"
date.toLocaleString('de-DE');
// "15.1.2024, 05:30:45"
date.toLocaleString('ja-JP');
// "2024/1/15 5:30:45"
// Locale with options
date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// "Monday, January 15, 2024 at 05:30 AM"
// Custom formatting with options
date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
// "01/15/2024"
date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
// "Mon, Jan 15"
// Parsing strings
Date.parse('2024-01-15T10:30:45.123Z'); // 1705318245123
Date.parse('2024-01-15'); // midnight UTC
Date.parse('Jan 15, 2024'); // implementation-dependent ⚠️
// Check if parse succeeded
const timestamp = Date.parse('invalid');
if (isNaN(timestamp)) {
console.log('Invalid date string');
}
// new Date() uses Date.parse()
new Date('2024-01-15T10:30:45.123Z');
// Date.UTC() - create UTC timestamp from components
const utcTimestamp = Date.UTC(2024, 0, 15, 10, 30, 45, 123);
// Same as new Date(2024, 0, 15, 10, 30, 45, 123) but UTC
const utcDate = new Date(Date.UTC(2024, 0, 15, 10, 30));
// Custom formatting (manual)
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
formatDate(new Date()); // "2024-01-15"
function formatDateTime(date) {
const dateStr = formatDate(date);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${dateStr} ${hours}:${minutes}:${seconds}`;
}
// Format with time zone
function formatWithTimezone(date) {
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? '+' : '-';
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
return `${formatDateTime(date)} GMT${sign}${hours}:${minutes}`;
}
// Relative time formatting
function getRelativeTime(date) {
const now = new Date();
const diffMs = now - date;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) return `${diffSec} seconds ago`;
if (diffMin < 60) return `${diffMin} minutes ago`;
if (diffHour < 24) return `${diffHour} hours ago`;
if (diffDay < 30) return `${diffDay} days ago`;
return date.toLocaleDateString();
}
// ISO 8601 variants
const d = new Date();
// Local ISO (no Z)
function toLocalISOString(date) {
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? '+' : '-';
const pad = (n) => String(n).padStart(2, '0');
return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate()) +
'T' + pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds()) +
'.' + String(date.getMilliseconds()).padStart(3, '0') +
sign + pad(Math.floor(Math.abs(offset) / 60)) +
':' + pad(Math.abs(offset) % 60);
}
// Common date formats
function formatters(date) {
return {
iso: date.toISOString(), // "2024-01-15T10:30:45.123Z"
date: date.toLocaleDateString(), // "1/15/2024"
time: date.toLocaleTimeString(), // "10:30:45 AM"
short: date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
}), // "Jan 15, 2024"
long: date.toLocaleString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
}) // "Monday, January 15, 2024"
};
}
Note:
toISOString() always outputs UTC (Z suffix). Date.parse() is
inconsistent - prefer ISO 8601 format. Use Intl.DateTimeFormat for advanced locale formatting.
13.3 Timezone Handling and UTC Operations
| Concept | Method/Approach | Description | Example |
|---|---|---|---|
| Local Time | get*() methods |
Methods without UTC prefix use local timezone | date.getHours() |
| UTC Time | getUTC*() methods |
Methods with UTC prefix use UTC timezone | date.getUTCHours() |
| Timezone Offset | getTimezoneOffset() |
Minutes difference from UTC (negative = ahead) | -300 for UTC-5 |
| UTC Constructor | Date.UTC(y, m, d, ...) |
Create timestamp from UTC components | Date.UTC(2024, 0, 15) |
| UTC String | toISOString() |
Always returns UTC time with Z suffix | "2024-01-15T10:30:45.123Z" |
| Timezone Name | Intl.DateTimeFormat |
Get timezone identifier | "America/New_York" |
Example: Timezone handling
// Create date in local timezone
const local = new Date('2024-01-15T10:30:00'); // local 10:30
// Create date in UTC (Z suffix)
const utc = new Date('2024-01-15T10:30:00Z'); // UTC 10:30
// Local vs UTC getters
const date = new Date('2024-01-15T10:30:00Z');
// Local time (depends on system timezone)
date.getHours(); // e.g., 5 (if UTC-5)
date.getMinutes(); // 30
// UTC time (always same regardless of timezone)
date.getUTCHours(); // 10
date.getUTCMinutes(); // 30
// Timezone offset (minutes from UTC)
date.getTimezoneOffset();
// -300 for UTC-5 (EST) - negative means behind UTC
// 0 for UTC
// 60 for UTC+1 (CET)
// Convert offset to hours
const offsetHours = -date.getTimezoneOffset() / 60;
// -5 for EST, 0 for UTC, +1 for CET
// Get timezone name (modern browsers)
const timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/New_York", "Europe/London", "Asia/Tokyo"
// UTC constructor
const utcTimestamp = Date.UTC(2024, 0, 15, 10, 30, 0);
const utcDate = new Date(utcTimestamp);
// Difference: local vs UTC constructor
const localDate = new Date(2024, 0, 15, 10, 30); // 10:30 local
const utcDate2 = new Date(Date.UTC(2024, 0, 15, 10, 30)); // 10:30 UTC
localDate.getTime() !== utcDate2.getTime(); // true (different times!)
// Convert local to UTC
function toUTC(date) {
return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
}
// Convert UTC to local
function toLocal(date) {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
}
// Get UTC date at midnight
function getUTCDate(date) {
return new Date(Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate()
));
}
// Get local date at midnight
function getLocalDate(date) {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
}
// Compare dates ignoring time
function isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
// Compare UTC dates
function isSameUTCDay(date1, date2) {
return date1.getUTCFullYear() === date2.getUTCFullYear() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCDate() === date2.getUTCDate();
}
// Format with timezone
function formatWithTZ(date) {
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? '+' : '-';
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
return date.toISOString().slice(0, -1) + // remove Z
`${sign}${hours}:${minutes}`;
}
// Parse ISO with timezone
const withTZ = new Date('2024-01-15T10:30:00+05:30'); // India
const withUTC = new Date('2024-01-15T10:30:00Z'); // UTC
withTZ.getTime() !== withUTC.getTime(); // different times
// Daylight Saving Time (DST) detection
function isDST(date) {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
return date.getTimezoneOffset() < stdOffset;
}
isDST(new Date('2024-01-15')); // false (winter)
isDST(new Date('2024-07-15')); // true (summer, in DST zones)
// Working with specific timezones (use Intl)
const nyDate = new Date('2024-01-15T10:30:00Z');
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
formatter.format(nyDate); // "01/15/2024, 05:30:00" (EST)
// Different timezone
const tokyoFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Tokyo',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
tokyoFormatter.format(nyDate); // "19:30" (JST, +9 hours)
// Get time in specific timezone
function getTimeInTimezone(date, timezone) {
return new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(date);
}
getTimeInTimezone(new Date(), 'America/Los_Angeles');
getTimeInTimezone(new Date(), 'Europe/London');
getTimeInTimezone(new Date(), 'Asia/Dubai');
// Best practices
// 1. Store dates as UTC timestamps
const stored = date.getTime(); // or date.toISOString()
// 2. Convert to local for display
const displayed = new Date(stored);
// 3. Always use UTC for date comparisons
const isAfter = date1.getTime() > date2.getTime();
// 4. Use libraries for complex timezone work
// moment-timezone, date-fns-tz, luxon
Warning: Timezone offset is negative when ahead of UTC (UTC-5 = -300 minutes). DST changes
offset during year. Always store dates as UTC timestamps. Date string without timezone = local time
(implementation-dependent).
13.4 Date Arithmetic and Calculations
| Operation | Method | Description | Example |
|---|---|---|---|
| Add Days | setDate(getDate() + n) |
Add/subtract days; auto-handles month/year rollover | date.setDate(date.getDate() + 7) |
| Add Months | setMonth(getMonth() + n) |
Add/subtract months; handles year rollover | date.setMonth(date.getMonth() + 3) |
| Add Years | setFullYear(getFullYear() + n) |
Add/subtract years | date.setFullYear(date.getFullYear() + 1) |
| Add Hours | setHours(getHours() + n) |
Add/subtract hours; handles day rollover | date.setHours(date.getHours() + 24) |
| Add Milliseconds | setTime(getTime() + ms) |
Precise time arithmetic using timestamps | date.setTime(date.getTime() + 1000) |
| Date Difference | date1 - date2 |
Subtract dates to get milliseconds difference | diff = date1 - date2 |
| Days Between | (date1 - date2) / MS_PER_DAY |
Convert millisecond difference to days | Math.floor(diff / 86400000) |
| Compare Dates | date1 < date2 |
Direct comparison operators work | if (date1 < date2) {...} |
Example: Date arithmetic
// Add/subtract days
const today = new Date('2024-01-15');
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1); // Jan 16
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1); // Jan 14
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7); // Jan 22
// Handles month/year rollover automatically
const endOfMonth = new Date('2024-01-31');
endOfMonth.setDate(endOfMonth.getDate() + 1); // Feb 1, 2024
// Add months
const date = new Date('2024-01-15');
date.setMonth(date.getMonth() + 1); // Feb 15, 2024
date.setMonth(date.getMonth() + 12); // Feb 15, 2025
// Month rollover edge case
const jan31 = new Date('2024-01-31');
jan31.setMonth(jan31.getMonth() + 1); // Feb 29 or Mar 2/3 (Feb has fewer days)
// Safe month addition (preserve day if possible)
function addMonths(date, months) {
const d = new Date(date);
const targetMonth = d.getMonth() + months;
const targetYear = d.getFullYear() + Math.floor(targetMonth / 12);
const finalMonth = ((targetMonth % 12) + 12) % 12;
d.setFullYear(targetYear);
d.setMonth(finalMonth);
// If day changed (e.g., Jan 31 + 1 month), go to last day of month
if (d.getDate() !== date.getDate()) {
d.setDate(0); // Last day of previous month
}
return d;
}
// Add years
const now = new Date();
const nextYear = new Date(now);
nextYear.setFullYear(nextYear.getFullYear() + 1);
// Add hours
const hour = new Date();
const nextHour = new Date(hour);
nextHour.setHours(nextHour.getHours() + 1);
// Add minutes
const minute = new Date();
const nextMinute = new Date(minute);
nextMinute.setMinutes(nextMinute.getMinutes() + 30);
// Precise arithmetic with timestamps
const MS_PER_SECOND = 1000;
const MS_PER_MINUTE = 60 * 1000;
const MS_PER_HOUR = 60 * 60 * 1000;
const MS_PER_DAY = 24 * 60 * 60 * 1000;
// Add 7 days using timestamps
const d = new Date();
d.setTime(d.getTime() + 7 * MS_PER_DAY);
// Alternative using Date constructor
const future = new Date(Date.now() + 7 * MS_PER_DAY);
// Date difference (returns milliseconds)
const date1 = new Date('2024-01-20');
const date2 = new Date('2024-01-15');
const diffMs = date1 - date2; // 432000000 ms (5 days)
// Convert to different units
const diffSeconds = diffMs / 1000;
const diffMinutes = diffMs / (1000 * 60);
const diffHours = diffMs / (1000 * 60 * 60);
const diffDays = diffMs / (1000 * 60 * 60 * 24);
// Days between dates (whole days)
function daysBetween(date1, date2) {
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const utc1 = Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate());
const utc2 = Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate());
return Math.floor((utc1 - utc2) / MS_PER_DAY);
}
daysBetween(new Date('2024-01-20'), new Date('2024-01-15')); // 5
// Compare dates
date1 < date2; // false
date1 > date2; // true
date1 === date2; // false (different objects)
date1.getTime() === date2.getTime(); // false (different times)
// Check if same day
function isSameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
// Business days calculation (excluding weekends)
function addBusinessDays(date, days) {
const result = new Date(date);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) { // Not Sunday or Saturday
added++;
}
}
return result;
}
// Count business days between dates
function businessDaysBetween(startDate, endDate) {
let count = 0;
const current = new Date(startDate);
while (current < endDate) {
const day = current.getDay();
if (day !== 0 && day !== 6) {
count++;
}
current.setDate(current.getDate() + 1);
}
return count;
}
// Age calculation
function calculateAge(birthDate, asOfDate = new Date()) {
let age = asOfDate.getFullYear() - birthDate.getFullYear();
const monthDiff = asOfDate.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && asOfDate.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
calculateAge(new Date('1990-05-15')); // e.g., 34
// Start/end of period
function startOfDay(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
function endOfDay(date) {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
return d;
}
function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
function endOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}
function startOfYear(date) {
return new Date(date.getFullYear(), 0, 1);
}
function endOfYear(date) {
return new Date(date.getFullYear(), 11, 31, 23, 59, 59, 999);
}
// Days in month
function daysInMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
}
daysInMonth(new Date('2024-02-01')); // 29 (leap year)
daysInMonth(new Date('2023-02-01')); // 28
// Is leap year
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
// Week number
function getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
// Elapsed time
function timeElapsed(startDate, endDate = new Date()) {
const diff = endDate - startDate;
const days = Math.floor(diff / MS_PER_DAY);
const hours = Math.floor((diff % MS_PER_DAY) / MS_PER_HOUR);
const minutes = Math.floor((diff % MS_PER_HOUR) / MS_PER_MINUTE);
const seconds = Math.floor((diff % MS_PER_MINUTE) / MS_PER_SECOND);
return {days, hours, minutes, seconds};
}
// Countdown timer
function countdown(targetDate) {
const now = new Date();
const diff = targetDate - now;
if (diff <= 0) return {expired: true};
return {
expired: false,
days: Math.floor(diff / MS_PER_DAY),
hours: Math.floor((diff % MS_PER_DAY) / MS_PER_HOUR),
minutes: Math.floor((diff % MS_PER_HOUR) / MS_PER_MINUTE),
seconds: Math.floor((diff % MS_PER_MINUTE) / MS_PER_SECOND)
};
}
Note: Date methods auto-adjust for overflow (e.g., Feb 30 → Mar 2). Use timestamps for precise
arithmetic. Subtraction returns milliseconds. Month edge cases need careful handling (Jan 31 + 1 month).
13.5 Internationalization Date Formatting
| Intl.DateTimeFormat Option | Values | Description | Example Output |
|---|---|---|---|
| dateStyle | full, long, medium, short | Preset date format style | "Monday, January 15, 2024" |
| timeStyle | full, long, medium, short | Preset time format style | "10:30:45 AM EST" |
| weekday | narrow, short, long | Day of week name | "M", "Mon", "Monday" |
| year | numeric, 2-digit | Year format | "2024", "24" |
| month | numeric, 2-digit, narrow, short, long | Month format | "1", "01", "J", "Jan", "January" |
| day | numeric, 2-digit | Day of month | "15", "15" |
| hour | numeric, 2-digit | Hour format | "10", "10" |
| minute | numeric, 2-digit | Minute format | "30", "30" |
| second | numeric, 2-digit | Second format | "45", "45" |
| timeZone | IANA timezone identifier | Display in specific timezone | "America/New_York" |
| timeZoneName | short, long, shortOffset, longOffset | Timezone display | "EST", "Eastern Standard Time" |
| hour12 | true, false | 12 or 24 hour format | "10:30 AM" vs "10:30" |
Example: Intl.DateTimeFormat usage
const date = new Date('2024-01-15T10:30:45.123Z');
// Basic formatting
const formatter = new Intl.DateTimeFormat('en-US');
formatter.format(date); // "1/15/2024"
// Different locales
new Intl.DateTimeFormat('en-US').format(date); // "1/15/2024"
new Intl.DateTimeFormat('en-GB').format(date); // "15/01/2024"
new Intl.DateTimeFormat('de-DE').format(date); // "15.1.2024"
new Intl.DateTimeFormat('ja-JP').format(date); // "2024/1/15"
new Intl.DateTimeFormat('ar-EG').format(date); // "١٥/١/٢٠٢٤"
// Date and time styles
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full'
}).format(date);
// "Monday, January 15, 2024"
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long'
}).format(date);
// "January 15, 2024"
new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium'
}).format(date);
// "Jan 15, 2024"
new Intl.DateTimeFormat('en-US', {
dateStyle: 'short'
}).format(date);
// "1/15/24"
new Intl.DateTimeFormat('en-US', {
timeStyle: 'full'
}).format(date);
// "5:30:45 AM Eastern Standard Time"
new Intl.DateTimeFormat('en-US', {
timeStyle: 'long'
}).format(date);
// "5:30:45 AM EST"
new Intl.DateTimeFormat('en-US', {
timeStyle: 'medium'
}).format(date);
// "5:30:45 AM"
new Intl.DateTimeFormat('en-US', {
timeStyle: 'short'
}).format(date);
// "5:30 AM"
// Combined date and time
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long',
timeStyle: 'short'
}).format(date);
// "January 15, 2024 at 5:30 AM"
// Custom options
new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
// "Monday, January 15, 2024, 05:30 AM"
// Weekday formats
new Intl.DateTimeFormat('en-US', {weekday: 'narrow'}).format(date); // "M"
new Intl.DateTimeFormat('en-US', {weekday: 'short'}).format(date); // "Mon"
new Intl.DateTimeFormat('en-US', {weekday: 'long'}).format(date); // "Monday"
// Month formats
new Intl.DateTimeFormat('en-US', {month: 'numeric'}).format(date); // "1"
new Intl.DateTimeFormat('en-US', {month: '2-digit'}).format(date); // "01"
new Intl.DateTimeFormat('en-US', {month: 'narrow'}).format(date); // "J"
new Intl.DateTimeFormat('en-US', {month: 'short'}).format(date); // "Jan"
new Intl.DateTimeFormat('en-US', {month: 'long'}).format(date); // "January"
// 12 vs 24 hour
new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(date);
// "05:30 AM"
new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date);
// "05:30"
// Timezone handling
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'full',
timeStyle: 'full'
}).format(date);
// "Monday, January 15, 2024 at 5:30:45 AM Eastern Standard Time"
new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Tokyo',
dateStyle: 'full',
timeStyle: 'full'
}).format(date);
// "Monday, January 15, 2024 at 7:30:45 PM Japan Standard Time"
new Intl.DateTimeFormat('en-US', {
timeZone: 'Europe/London',
dateStyle: 'long',
timeStyle: 'long'
}).format(date);
// "January 15, 2024 at 10:30:45 AM GMT"
// Timezone name formats
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
timeZoneName: 'short'
}).format(date);
// "1/15/2024, EST"
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
timeZoneName: 'long'
}).format(date);
// "1/15/2024, Eastern Standard Time"
// Format parts (advanced)
const parts = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).formatToParts(date);
// [{type: 'month', value: 'January'}, {type: 'literal', value: ' '}, ...]
// Custom formatting using parts
function customFormat(date) {
const parts = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).formatToParts(date);
const obj = {};
parts.forEach(({type, value}) => {
obj[type] = value;
});
return `${obj.day} of ${obj.month}, ${obj.year}`;
}
customFormat(date); // "15 of January, 2024"
// Format range (multiple dates)
const start = new Date('2024-01-15');
const end = new Date('2024-01-20');
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long'
}).formatRange(start, end);
// "January 15 – 20, 2024"
// Range across months
const start2 = new Date('2024-01-28');
const end2 = new Date('2024-02-05');
new Intl.DateTimeFormat('en-US', {
dateStyle: 'long'
}).formatRange(start2, end2);
// "January 28 – February 5, 2024"
// Relative time formatting
const rtf = new Intl.RelativeTimeFormat('en-US', {numeric: 'auto'});
rtf.format(-1, 'day'); // "yesterday"
rtf.format(0, 'day'); // "today"
rtf.format(1, 'day'); // "tomorrow"
rtf.format(2, 'day'); // "in 2 days"
rtf.format(-2, 'week'); // "2 weeks ago"
rtf.format(3, 'month'); // "in 3 months"
// Numeric style
const rtfNumeric = new Intl.RelativeTimeFormat('en-US', {numeric: 'always'});
rtfNumeric.format(-1, 'day'); // "1 day ago" (not "yesterday")
rtfNumeric.format(0, 'day'); // "in 0 days" (not "today")
// Get relative time
function getRelativeTime(date) {
const now = new Date();
const diff = date - now;
const absDiff = Math.abs(diff);
const rtf = new Intl.RelativeTimeFormat('en-US', {numeric: 'auto'});
if (absDiff < 60 * 1000) {
return rtf.format(Math.round(diff / 1000), 'second');
}
if (absDiff < 60 * 60 * 1000) {
return rtf.format(Math.round(diff / (60 * 1000)), 'minute');
}
if (absDiff < 24 * 60 * 60 * 1000) {
return rtf.format(Math.round(diff / (60 * 60 * 1000)), 'hour');
}
if (absDiff < 30 * 24 * 60 * 60 * 1000) {
return rtf.format(Math.round(diff / (24 * 60 * 60 * 1000)), 'day');
}
return rtf.format(Math.round(diff / (30 * 24 * 60 * 60 * 1000)), 'month');
}
// Reusable formatter
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
dateFormatter.format(date1);
dateFormatter.format(date2);
dateFormatter.format(date3);
// Get supported locales
Intl.DateTimeFormat.supportedLocalesOf(['en-US', 'de-DE', 'ja-JP']);
// ['en-US', 'de-DE', 'ja-JP']
Note:
Intl.DateTimeFormat is locale-aware and timezone-aware. Use
formatRange() for date ranges. Intl.RelativeTimeFormat for "2 days ago" style. Reuse
formatters for performance.
Section 13 Summary
- Creation:
new Date()current; from timestamp/string/components; month is 0-indexed (0=Jan); use ISO 8601 for consistency;Date.now()for timestamp - Getters/Setters:
get*/set*local time;getUTC*/setUTC*UTC;getTime()timestamp; setters auto-adjust overflow;getTimezoneOffset()minutes from UTC (negative = ahead) - Formatting:
toISOString()UTC standard;toLocaleString()locale-aware;toString()local;toJSON()serialization;formatToParts()for custom formats - Timezones: Local vs UTC methods; offset in minutes; store as UTC
timestamps; display in local;
Intl.DateTimeFormatwithtimeZoneoption; DST affects offset - Arithmetic:
setDate/Month/Yearauto-rollover; timestamp math for precision; subtract dates = milliseconds; convert using MS_PER_DAY;daysBetween()use UTC dates - Intl.DateTimeFormat: Locale-aware formatting;
dateStyle/timeStylepresets; custom options (weekday/month/year);timeZonefor specific zones;formatRange()for periods; reuse formatters - Best practices: Store UTC timestamps; display in local; use ISO 8601; handle DST; beware month edge cases (Jan 31+1mo); use libraries (date-fns, luxon) for complex operations
14. Asynchronous Programming Mastery
14.1 Callback Patterns and Error Handling
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Error-first Callback | (err, result) => {...} |
Node.js convention: first param is error (null if success) | File I/O, network requests |
| Success Callback | (result) => {...} |
Browser APIs: callback receives result on success | setTimeout, event handlers |
| Separate Callbacks | onSuccess, onError |
Two separate functions for success and error | AJAX, animations |
| Callback Hell | Nested callbacks | Deep nesting; hard to read/maintain | Anti-pattern (avoid!) |
| Named Functions | Extract callbacks to functions | Flatten callback structure; improve readability | Complex async flows |
Example: Callback patterns
// Error-first callback (Node.js style)
function readFile(path, callback) {
// Simulated async operation
setTimeout(() => {
if (!path) {
callback(new Error('Path is required'), null);
} else {
callback(null, 'file contents');
}
}, 100);
}
// Usage
readFile('file.txt', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Data:', data);
});
// Success callback (browser style)
setTimeout(() => {
console.log('Executed after 1 second');
}, 1000);
// Separate success/error callbacks
function fetchData(url, onSuccess, onError) {
setTimeout(() => {
if (url.includes('fail')) {
onError(new Error('Request failed'));
} else {
onSuccess({data: 'response'});
}
}, 100);
}
fetchData('api/data',
(result) => console.log('Success:', result),
(error) => console.error('Error:', error)
);
// ❌ Callback Hell (pyramid of doom)
getData((data1) => {
processData(data1, (data2) => {
saveData(data2, (data3) => {
sendNotification(data3, (result) => {
console.log('All done!');
});
});
});
});
// ✓ Named functions (flattened)
function handleData1(data1) {
processData(data1, handleData2);
}
function handleData2(data2) {
saveData(data2, handleData3);
}
function handleData3(data3) {
sendNotification(data3, handleFinal);
}
function handleFinal(result) {
console.log('All done!');
}
getData(handleData1);
// Error handling patterns
function asyncOperation(callback) {
try {
setTimeout(() => {
// Errors in setTimeout won't be caught by outer try-catch!
throw new Error('Async error');
}, 100);
} catch (error) {
// This won't catch the error ❌
callback(error, null);
}
}
// Correct: handle errors inside async code
function asyncOperationCorrect(callback) {
setTimeout(() => {
try {
// Do work
const result = riskyOperation();
callback(null, result);
} catch (error) {
callback(error, null);
}
}, 100);
}
// Multiple callbacks pattern
function multiStep(callback) {
step1((err, result1) => {
if (err) return callback(err);
step2(result1, (err, result2) => {
if (err) return callback(err);
step3(result2, (err, result3) => {
if (err) return callback(err);
callback(null, result3);
});
});
});
}
// Parallel callbacks with counter
function parallelTasks(tasks, callback) {
let completed = 0;
const results = [];
let hasError = false;
tasks.forEach((task, index) => {
task((err, result) => {
if (hasError) return;
if (err) {
hasError = true;
return callback(err);
}
results[index] = result;
completed++;
if (completed === tasks.length) {
callback(null, results);
}
});
});
}
// Usage
parallelTasks([
(cb) => setTimeout(() => cb(null, 'task1'), 100),
(cb) => setTimeout(() => cb(null, 'task2'), 50),
(cb) => setTimeout(() => cb(null, 'task3'), 75)
], (err, results) => {
console.log(results); // ['task1', 'task2', 'task3']
});
// Callback with context preservation
function Timer(duration, callback) {
this.duration = duration;
this.callback = callback;
}
Timer.prototype.start = function() {
// Preserve 'this' context
setTimeout(() => {
this.callback(this.duration);
}, this.duration);
};
// Timeout wrapper
function withTimeout(asyncFn, timeout, callback) {
let called = false;
const timer = setTimeout(() => {
if (!called) {
called = true;
callback(new Error('Timeout'), null);
}
}, timeout);
asyncFn((err, result) => {
if (!called) {
called = true;
clearTimeout(timer);
callback(err, result);
}
});
}
Warning: Callbacks inside async operations can't be caught by outer try-catch. Always handle
errors inside the async code. Avoid callback hell - use Promises or async/await. Remember to call callback
exactly once.
14.2 Promise API and Promise Chaining
| Method | Syntax | Description | Returns |
|---|---|---|---|
| Constructor | new Promise((resolve, reject) => {...}) |
Create new Promise; executor runs immediately | Promise |
| then() | promise.then(onFulfilled, onRejected) |
Attach fulfillment/rejection handlers; returns new Promise | Promise |
| catch() | promise.catch(onRejected) |
Catch errors; shorthand for .then(null, onRejected) |
Promise |
| finally() | promise.finally(onFinally) |
Runs regardless of outcome; no arguments | Promise |
| Promise.resolve() | Promise.resolve(value) |
Create fulfilled Promise with value | Promise |
| Promise.reject() | Promise.reject(reason) |
Create rejected Promise with reason | Promise |
Example: Promise creation and chaining
// Creating a Promise
const promise = new Promise((resolve, reject) => {
// Executor function runs immediately
setTimeout(() => {
const success = true;
if (success) {
resolve('Success!'); // Fulfill
} else {
reject(new Error('Failed!')); // Reject
}
}, 1000);
});
// Consuming with then/catch
promise
.then(result => {
console.log(result); // 'Success!'
return result.toUpperCase();
})
.then(upper => {
console.log(upper); // 'SUCCESS!'
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log('Cleanup'); // Always runs
});
// Promise states
// 1. Pending: initial state
// 2. Fulfilled: operation completed successfully
// 3. Rejected: operation failed
// Promise.resolve - create fulfilled Promise
Promise.resolve(42)
.then(value => console.log(value)); // 42
// Resolving with another Promise
Promise.resolve(Promise.resolve(42))
.then(value => console.log(value)); // 42 (unwrapped)
// Promise.reject - create rejected Promise
Promise.reject(new Error('Failed'))
.catch(error => console.error(error));
// Chaining - each then returns new Promise
fetch('/api/user')
.then(response => response.json()) // Parse JSON
.then(data => data.userId) // Extract userId
.then(userId => fetch(`/api/profile/${userId}`)) // Fetch profile
.then(response => response.json())
.then(profile => console.log(profile))
.catch(error => console.error('Error:', error));
// Return value determines next Promise
Promise.resolve(1)
.then(x => x + 1) // Returns 2 (wrapped in Promise)
.then(x => x * 2) // Returns 4
.then(x => console.log(x)); // 4
// Returning a Promise flattens the chain
Promise.resolve(1)
.then(x => Promise.resolve(x + 1)) // Returns Promise
.then(x => console.log(x)); // 2 (not Promise!)
// Error propagation
Promise.resolve(1)
.then(x => {
throw new Error('Oops!');
})
.then(x => {
console.log('Skipped'); // Never executes
})
.catch(error => {
console.error(error.message); // 'Oops!'
return 'recovered';
})
.then(value => {
console.log(value); // 'recovered' - chain continues
});
// finally() doesn't receive value/error
Promise.resolve(42)
.finally(() => {
console.log('Cleanup'); // No arguments
return 'ignored'; // Return value ignored
})
.then(value => console.log(value)); // 42 (original value)
// finally() propagates rejection
Promise.reject(new Error('Failed'))
.finally(() => {
console.log('Cleanup');
})
.catch(error => console.error(error)); // Original error
// Multiple handlers on same Promise
const p = Promise.resolve(42);
p.then(x => console.log('Handler 1:', x));
p.then(x => console.log('Handler 2:', x));
// Both execute independently
// Catching errors in specific parts
fetch('/api/data')
.then(response => response.json())
.catch(error => {
console.error('Fetch failed:', error);
return {fallback: true}; // Provide fallback
})
.then(data => {
// Process data or fallback
if (data.fallback) {
console.log('Using fallback');
}
});
// Converting callbacks to Promises
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// Usage
const readFilePromise = promisify(fs.readFile);
readFilePromise('file.txt', 'utf8')
.then(contents => console.log(contents))
.catch(error => console.error(error));
// Thenable objects (Promise-like)
const thenable = {
then(onFulfilled, onRejected) {
setTimeout(() => onFulfilled(42), 100);
}
};
Promise.resolve(thenable)
.then(value => console.log(value)); // 42
// Sequential execution
function sequential(promises) {
return promises.reduce(
(chain, promise) => chain.then(() => promise()),
Promise.resolve()
);
}
// Parallel with chaining
Promise.resolve()
.then(() => fetch('/api/data1'))
.then(r => r.json())
.then(data1 => {
// Now fetch data2 using data1
return fetch(`/api/data2/${data1.id}`);
})
.then(r => r.json())
.then(data2 => console.log(data2));
Note: Promises are chainable - each
then() returns a new Promise. Errors bubble to
nearest catch(). finally() doesn't receive arguments. Return values are auto-wrapped
in Promises.
14.3 async/await Syntax and Error Handling
| Keyword | Syntax | Description | Returns |
|---|---|---|---|
| async function | async function name() {...} |
Declares async function; always returns Promise | Promise |
| await | await promise |
Pause until Promise settles; only in async functions | Resolved value |
| async arrow | async () => {...} |
Async arrow function | Promise |
| async method | async methodName() {...} |
Async method in class/object | Promise |
| try/catch | try {await ...} catch (e) {...} |
Handle async errors synchronously | - |
Example: async/await usage
// Basic async function
async function fetchUser() {
const response = await fetch('/api/user');
const data = await response.json();
return data; // Automatically wrapped in Promise
}
// Usage
fetchUser()
.then(user => console.log(user))
.catch(error => console.error(error));
// Or await the async function
async function main() {
const user = await fetchUser();
console.log(user);
}
// Return value is wrapped in Promise
async function getValue() {
return 42; // Becomes Promise.resolve(42)
}
getValue().then(x => console.log(x)); // 42
// Equivalent to:
function getValuePromise() {
return Promise.resolve(42);
}
// Throwing in async function = rejected Promise
async function throwError() {
throw new Error('Failed!');
}
throwError()
.catch(error => console.error(error)); // Error: Failed!
// try/catch for error handling
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error; // Re-throw or return fallback
}
}
// Sequential execution (waits for each)
async function sequential() {
const result1 = await fetch('/api/data1');
const data1 = await result1.json();
const result2 = await fetch('/api/data2');
const data2 = await result2.json();
const result3 = await fetch('/api/data3');
const data3 = await result3.json();
return [data1, data2, data3];
}
// Parallel execution (start all at once)
async function parallel() {
// Start all requests simultaneously
const [result1, result2, result3] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2'),
fetch('/api/data3')
]);
// Parse in parallel
const [data1, data2, data3] = await Promise.all([
result1.json(),
result2.json(),
result3.json()
]);
return [data1, data2, data3];
}
// Conditional await
async function conditionalFetch(useCache) {
let data;
if (useCache) {
data = getFromCache();
} else {
const response = await fetch('/api/data');
data = await response.json();
}
return data;
}
// await in loops
async function processItems(items) {
// Sequential processing
for (const item of items) {
const result = await processItem(item);
console.log(result);
}
// Parallel processing (better for independent operations)
const results = await Promise.all(
items.map(item => processItem(item))
);
}
// Error handling patterns
async function withErrorHandling() {
try {
const data = await fetchData();
return {success: true, data};
} catch (error) {
return {success: false, error: error.message};
}
}
// Multiple try/catch blocks
async function multipleOperations() {
let userData, postsData;
try {
userData = await fetchUser();
} catch (error) {
console.error('User fetch failed:', error);
userData = getDefaultUser();
}
try {
postsData = await fetchPosts(userData.id);
} catch (error) {
console.error('Posts fetch failed:', error);
postsData = [];
}
return {user: userData, posts: postsData};
}
// finally block
async function withFinally() {
const loading = showLoader();
try {
const data = await fetchData();
return data;
} catch (error) {
showError(error);
throw error;
} finally {
hideLoader(loading); // Always runs
}
}
// Async arrow functions
const asyncArrow = async () => {
const result = await someAsyncOperation();
return result;
};
// Async methods
class DataService {
async fetchData() {
const response = await fetch('/api/data');
return response.json();
}
async saveData(data) {
const response = await fetch('/api/data', {
method: 'POST',
body: JSON.stringify(data)
});
return response.json();
}
}
// Async IIFE
(async () => {
const data = await fetchData();
console.log(data);
})();
// Top-level await (ES2022, in modules)
// const data = await fetchData(); // No async function needed
// Await non-Promise values (wrapped automatically)
async function awaitValues() {
const a = await 42; // Promise.resolve(42)
const b = await Promise.resolve(10);
return a + b; // 52
}
// Error handling without try/catch (propagates)
async function propagateError() {
const data = await fetchData(); // If rejected, function rejects
return data;
}
propagateError()
.then(data => console.log(data))
.catch(error => console.error(error));
// Timeout with async/await
async function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
// Usage
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
}
}
// Retry logic
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retry(() => fetchData(), 3, 2000);
Warning:
await only works inside async functions (except top-level
await in modules). Sequential awaits block execution - use Promise.all() for parallel. Errors
propagate unless caught.
14.4 Promise Combinators (all, race, allSettled, any)
| Combinator | Resolves When | Rejects When | Use Case |
|---|---|---|---|
| Promise.all() | All promises fulfill | Any promise rejects (fail-fast) | Parallel operations, all required |
| Promise.race() | First promise fulfills | First promise rejects | Timeouts, fastest response |
| Promise.allSettled() ES2020 | All promises settle (fulfill or reject) | Never rejects | Independent operations, all results needed |
| Promise.any() ES2021 | First promise fulfills | All promises reject (AggregateError) | First successful result, fallbacks |
Example: Promise combinators
// Promise.all - wait for all (fail-fast)
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3]
});
// With async/await
async function fetchAll() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return {user, posts, comments};
}
// Promise.all fails fast
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('Failed!')),
Promise.resolve(3) // Never executes
])
.catch(error => {
console.error(error); // Error: Failed!
});
// Promise.race - first to settle wins
Promise.race([
fetch('/api/fast'),
fetch('/api/slow')
])
.then(result => {
console.log('Fastest response:', result);
});
// Timeout with race
async function fetchWithTimeout(url, timeout) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
try {
const response = await fetchWithTimeout('/api/data', 5000);
} catch (error) {
console.error('Request timed out or failed');
}
// Promise.allSettled - wait for all, never rejects
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('Failed')),
Promise.resolve(3)
])
.then(results => {
console.log(results);
// [
// {status: 'fulfilled', value: 1},
// {status: 'rejected', reason: Error: Failed},
// {status: 'fulfilled', value: 3}
// ]
});
// Process results
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.allSettled(promises);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
return {successful, failed};
}
// Promise.any - first success (ES2021)
Promise.any([
fetch('/api/server1'),
fetch('/api/server2'),
fetch('/api/server3')
])
.then(response => {
console.log('First successful response:', response);
})
.catch(error => {
console.error('All requests failed:', error);
});
// Fallback pattern with any
async function fetchWithFallback(urls) {
try {
return await Promise.any(
urls.map(url => fetch(url).then(r => r.json()))
);
} catch (error) {
// AggregateError - all promises rejected
console.error('All sources failed:', error.errors);
throw new Error('No data available');
}
}
// Compare: race vs any
// race - first to settle (fulfill or reject)
Promise.race([
Promise.reject(new Error('Fast fail')),
Promise.resolve('Slow success')
])
.catch(error => console.error(error)); // Fast fail wins
// any - first to fulfill (ignores rejections until all fail)
Promise.any([
Promise.reject(new Error('Fast fail')),
Promise.resolve('Slow success')
])
.then(result => console.log(result)); // Slow success wins
// Practical examples
// 1. Parallel data fetching with all
async function loadPage() {
const [header, content, sidebar] = await Promise.all([
fetchHeader(),
fetchContent(),
fetchSidebar()
]);
renderPage(header, content, sidebar);
}
// 2. Progress tracking with allSettled
async function batchProcess(items) {
const promises = items.map(item => processItem(item));
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Processed: ${successful} success, ${failed} failed`);
return results;
}
// 3. Request racing
async function fastestSource(sources) {
return Promise.race(
sources.map(source => fetch(source).then(r => r.json()))
);
}
// 4. Redundant requests with any
async function reliableFetch(mirrors) {
try {
return await Promise.any(
mirrors.map(mirror => fetch(mirror).then(r => r.json()))
);
} catch (aggregateError) {
console.error('All mirrors failed:', aggregateError.errors);
throw new Error('Service unavailable');
}
}
// 5. Mixed operations with allSettled
async function initializeApp() {
const results = await Promise.allSettled([
loadCriticalData(), // Must succeed
loadUserPreferences(), // Optional
loadAnalytics(), // Optional
connectWebSocket() // Optional
]);
// Handle critical failure
if (results[0].status === 'rejected') {
throw new Error('Critical initialization failed');
}
// Continue with partial data
return processResults(results);
}
// Empty array behavior
Promise.all([]); // Resolves to []
Promise.race([]); // Never settles (hangs forever!)
Promise.allSettled([]); // Resolves to []
Promise.any([]); // Rejects with AggregateError
// Non-promise values (auto-wrapped)
Promise.all([1, 2, Promise.resolve(3)])
.then(results => console.log(results)); // [1, 2, 3]
// Combining combinators
async function complexOperation() {
// First get fastest mirror
const data = await Promise.race([
fetch('/mirror1'),
fetch('/mirror2')
]);
// Then fetch related data in parallel
const [details, related] = await Promise.all([
fetch(`/details/${data.id}`),
fetch(`/related/${data.id}`)
]);
return {data, details, related};
}
Note:
Promise.all() fails fast (first rejection).
Promise.allSettled() never rejects. Promise.race() first to settle.
Promise.any() first to fulfill. Empty array in race() hangs forever!
14.5 Event Loop and Task Queue Management
| Component | Description | Priority | Examples |
|---|---|---|---|
| Call Stack | Synchronous code execution; LIFO (Last In First Out) | Highest | Function calls, variable assignments |
| Microtask Queue | High priority async tasks; runs after current task, before macrotasks | High | Promise callbacks, queueMicrotask |
| Macrotask Queue | Lower priority async tasks; one per event loop tick | Normal | setTimeout, setInterval, I/O |
| Animation Frame | Visual updates before next repaint; ~60fps | Special | requestAnimationFrame |
| Event Loop | Orchestrates execution: call stack → microtasks → render → macrotask | - | JavaScript runtime |
Example: Event loop execution order
// Execution order demonstration
console.log('1: Synchronous');
setTimeout(() => {
console.log('2: Macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Microtask (Promise)');
});
console.log('4: Synchronous');
// Output order:
// 1: Synchronous
// 4: Synchronous
// 3: Microtask (Promise)
// 2: Macrotask (setTimeout)
// Event loop phases:
// 1. Execute all synchronous code (call stack)
// 2. Execute all microtasks
// 3. Render (if needed)
// 4. Execute one macrotask
// 5. Repeat from step 2
// Complex example
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 2'), 0);
});
console.log('End');
// Output:
// Start
// End
// Promise 2
// Timeout 1
// Promise 1
// Timeout 2
// Microtasks drain completely before next macrotask
Promise.resolve().then(() => {
console.log('Microtask 1');
Promise.resolve().then(() => {
console.log('Microtask 2');
Promise.resolve().then(() => {
console.log('Microtask 3');
});
});
});
setTimeout(() => console.log('Macrotask'), 0);
// Output:
// Microtask 1
// Microtask 2
// Microtask 3
// Macrotask (only after ALL microtasks complete)
// queueMicrotask API
queueMicrotask(() => {
console.log('Microtask via queueMicrotask');
});
Promise.resolve().then(() => {
console.log('Microtask via Promise');
});
// Both have same priority (microtasks)
// Blocking the event loop (BAD!)
function blockFor(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// Blocks event loop - nothing else can run!
}
}
console.log('Before block');
blockFor(3000); // Blocks for 3 seconds
console.log('After block');
// setTimeout won't execute during block
setTimeout(() => console.log('Delayed'), 0);
// Call stack visualization
function first() {
console.log('First function');
second();
console.log('First function end');
}
function second() {
console.log('Second function');
third();
console.log('Second function end');
}
function third() {
console.log('Third function');
}
first();
// Call stack:
// first() pushed
// second() pushed
// third() pushed
// third() popped (executed)
// second() popped (executed)
// first() popped (executed)
// Async breaks out of call stack
function asyncFirst() {
console.log('Async first');
setTimeout(() => {
console.log('Async second (later)');
}, 0);
console.log('Async first end');
}
asyncFirst();
// Output:
// Async first
// Async first end
// Async second (later) - after event loop cycle
// Infinite microtask (BAD! - blocks rendering)
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotasks(); // Never lets event loop continue!
});
}
// DON'T DO THIS - page will freeze
// infiniteMicrotasks();
// Task priorities
async function taskPriorities() {
console.log('1: Sync');
// Macrotask
setTimeout(() => console.log('5: Macrotask'), 0);
// Microtask (Promise)
Promise.resolve().then(() => console.log('3: Microtask Promise'));
// Microtask (queueMicrotask)
queueMicrotask(() => console.log('4: Microtask queue'));
// Sync await
await Promise.resolve();
console.log('2: After await (microtask)');
}
taskPriorities();
// Long-running task splitting
function processLargeArray(items) {
let index = 0;
function processChunk() {
const chunkSize = 100;
const end = Math.min(index + chunkSize, items.length);
for (; index < end; index++) {
// Process item
processItem(items[index]);
}
if (index < items.length) {
// Schedule next chunk (lets event loop breathe)
setTimeout(processChunk, 0);
} else {
console.log('Processing complete');
}
}
processChunk();
}
// Yielding to event loop
async function yieldToEventLoop() {
await new Promise(resolve => setTimeout(resolve, 0));
}
async function longTask() {
for (let i = 0; i < 1000; i++) {
doWork(i);
// Yield every 100 iterations
if (i % 100 === 0) {
await yieldToEventLoop();
}
}
}
// Measuring event loop lag
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastTime - 100; // Expected: 100ms
if (lag > 10) {
console.warn(`Event loop lag: ${lag}ms`);
}
lastTime = now;
}, 100);
// Web Worker for heavy computation (doesn't block main thread)
const worker = new Worker('worker.js');
worker.postMessage({task: 'heavy-computation', data: largeDataset});
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
Warning: All microtasks execute before next macrotask - can block rendering! Don't block event
loop with long sync operations. Use Web Workers for heavy computation. Infinite microtasks freeze the page.
14.6 Microtasks vs Macrotasks Execution Order
| Type | APIs | Execution Timing | Characteristics |
|---|---|---|---|
| Microtasks | Promise.then/catch/finally, queueMicrotask, async/await, MutationObserver | After current task, before render | All drain before next macrotask; high priority |
| Macrotasks | setTimeout, setInterval, setImmediate (Node), I/O, UI rendering, postMessage | Next event loop tick | One per tick; lower priority |
| Render Steps | requestAnimationFrame, style calc, layout, paint | After microtasks, before next macrotask | Browser optimization; ~60fps |
Example: Microtask vs macrotask behavior
// Classic example
console.log('Script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('Script end');
// Output order:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout
// Why?
// 1. Sync code executes: "Script start", "Script end"
// 2. Call stack empty → drain microtask queue: "Promise 1", "Promise 2"
// 3. Microtasks done → execute one macrotask: "setTimeout"
// Nested example
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise in Timeout 1');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout in Promise 1');
}, 0);
});
// Output:
// Promise 1
// Timeout 1
// Promise in Timeout 1
// Timeout in Promise 1
// Timeout 2
// Detailed breakdown:
// Initial: microtask queue = [Promise 1], macrotask queue = [Timeout 1]
// Execute microtasks: "Promise 1" (adds Timeout in Promise 1 to macrotasks)
// Execute macrotask: "Timeout 1" (adds Promise in Timeout 1, Timeout 2)
// Execute microtasks: "Promise in Timeout 1"
// Execute macrotask: "Timeout in Promise 1"
// Execute macrotask: "Timeout 2"
// queueMicrotask vs setTimeout
console.log('Start');
setTimeout(() => console.log('Macro 1'), 0);
queueMicrotask(() => console.log('Micro 1'));
setTimeout(() => console.log('Macro 2'), 0);
queueMicrotask(() => console.log('Micro 2'));
console.log('End');
// Output:
// Start
// End
// Micro 1
// Micro 2
// Macro 1
// Macro 2
// Promise chaining creates microtasks
Promise.resolve()
.then(() => {
console.log('Then 1');
return Promise.resolve(); // Creates microtask
})
.then(() => console.log('Then 2'));
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Then 1
// Then 2
// Timeout
// async/await creates microtasks
async function example() {
console.log('Async start');
await Promise.resolve(); // Creates microtask
console.log('After await');
}
example();
console.log('Sync');
// Output:
// Async start
// Sync
// After await
// Microtask queue never empties if you keep adding
let count = 0;
function scheduleMicrotask() {
if (count < 5) {
queueMicrotask(() => {
console.log(`Microtask ${++count}`);
scheduleMicrotask(); // Schedule another
});
}
}
scheduleMicrotask();
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Microtask 1
// Microtask 2
// Microtask 3
// Microtask 4
// Microtask 5
// Timeout (only after all microtasks)
// Rendering blocked by microtasks
button.addEventListener('click', () => {
// Update UI
element.textContent = 'Processing...';
// This microtask chain blocks rendering
Promise.resolve()
.then(() => heavyWork1())
.then(() => heavyWork2())
.then(() => heavyWork3());
// User won't see "Processing..." until all work done!
});
// Better: use setTimeout to allow render
button.addEventListener('click', () => {
element.textContent = 'Processing...';
// Allow render before processing
setTimeout(() => {
Promise.resolve()
.then(() => heavyWork1())
.then(() => heavyWork2())
.then(() => heavyWork3())
.then(() => {
element.textContent = 'Done!';
});
}, 0);
});
// Order comparison
function orderTest() {
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
(async () => {
console.log('5');
await null;
console.log('6');
})();
console.log('7');
}
orderTest();
// Output: 1, 5, 7, 3, 4, 6, 2
// MutationObserver (microtask)
const observer = new MutationObserver(() => {
console.log('DOM changed (microtask)');
});
observer.observe(document.body, {childList: true});
console.log('Before mutation');
document.body.appendChild(document.createElement('div'));
console.log('After mutation');
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Before mutation
// After mutation
// DOM changed (microtask)
// Timeout
// Event handlers are macrotasks
button.addEventListener('click', () => {
console.log('Click handler (macrotask)');
Promise.resolve().then(() => {
console.log('Promise in handler (microtask)');
});
});
// When button clicked:
// Click handler (macrotask)
// Promise in handler (microtask)
// setImmediate (Node.js only) vs setTimeout
// setImmediate: executes after I/O events
// setTimeout(0): executes in next timer phase
// Practical: debouncing with microtasks
let pending = false;
function debounceMicrotask(fn) {
if (!pending) {
pending = true;
queueMicrotask(() => {
pending = false;
fn();
});
}
}
// Multiple calls in same task execute once
debounceMicrotask(() => console.log('Called'));
debounceMicrotask(() => console.log('Called'));
debounceMicrotask(() => console.log('Called'));
// Output: "Called" (only once)
// Comparison table
const examples = {
microtask: () => Promise.resolve().then(() => console.log('micro')),
macrotask: () => setTimeout(() => console.log('macro'), 0),
sync: () => console.log('sync')
};
examples.sync(); // Executes immediately
examples.microtask(); // Next (after sync)
examples.macrotask(); // Last (next tick)
// Output: sync, micro, macro
Note: Microtasks execute before rendering and next macrotask. All microtasks drain before one
macrotask. Use macrotasks (setTimeout) to allow rendering. Promise callbacks are microtasks.
14.7 Timer Functions (setTimeout, setInterval, requestAnimationFrame)
| Function | Syntax | Description | Use Case |
|---|---|---|---|
| setTimeout | setTimeout(fn, ms, ...args) |
Execute once after delay; returns timer ID | Delays, debouncing, async breaks |
| clearTimeout | clearTimeout(id) |
Cancel setTimeout before execution | Cancel pending timers |
| setInterval | setInterval(fn, ms, ...args) |
Execute repeatedly at interval; returns timer ID | Polling, periodic updates |
| clearInterval | clearInterval(id) |
Stop setInterval execution | Stop periodic timers |
| requestAnimationFrame | requestAnimationFrame(fn) |
Execute before next repaint; ~60fps; returns ID | Smooth animations, visual updates |
| cancelAnimationFrame | cancelAnimationFrame(id) |
Cancel pending animation frame | Stop animations |
Example: Timer functions
// setTimeout - execute once
setTimeout(() => {
console.log('Executed after 1 second');
}, 1000);
// With arguments
setTimeout((name, age) => {
console.log(`${name} is ${age}`);
}, 1000, 'Alice', 30);
// Return value is timer ID
const timerId = setTimeout(() => {
console.log('This might not execute');
}, 1000);
// Cancel before execution
clearTimeout(timerId);
// Minimum delay is ~4ms (browser throttling)
setTimeout(() => console.log('Actually ~4ms'), 0);
// setInterval - execute repeatedly
const intervalId = setInterval(() => {
console.log('Every second');
}, 1000);
// Stop after 5 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log('Interval stopped');
}, 5000);
// ⚠️ setInterval drift problem
let count = 0;
setInterval(() => {
console.log(`Count: ${++count}`);
// If this takes 50ms, next call is 950ms away
// Drift accumulates over time!
}, 1000);
// Better: recursive setTimeout (self-correcting)
let startTime = Date.now();
let count = 0;
function accurateInterval() {
count++;
console.log(`Count: ${count}`);
// Calculate next delay to maintain accuracy
const elapsed = Date.now() - startTime;
const nextDelay = (count * 1000) - elapsed;
setTimeout(accurateInterval, Math.max(0, nextDelay));
}
accurateInterval();
// Simple recursive setTimeout
function repeat() {
console.log('Every second');
setTimeout(repeat, 1000);
}
repeat();
// requestAnimationFrame - smooth animations
function animate() {
// Update animation
element.style.left = position + 'px';
position += 1;
// Continue animation
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
// RAF with timestamp
function animateWithTime(timestamp) {
console.log('Time since page load:', timestamp);
// Smooth animation based on time
const progress = timestamp / 1000; // seconds
element.style.left = (progress * 100) + 'px';
if (progress < 5) {
requestAnimationFrame(animateWithTime);
}
}
requestAnimationFrame(animateWithTime);
// Delta time for frame-independent animation
let lastTime = 0;
function gameLoop(currentTime) {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Move based on time elapsed (60fps = ~16ms)
position += speed * (deltaTime / 1000);
render();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
// Cancel animation
const rafId = requestAnimationFrame(animate);
cancelAnimationFrame(rafId);
// Debouncing with setTimeout
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Usage
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttling with setTimeout
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
// Usage
const throttledScroll = throttle(() => {
console.log('Scroll handler');
}, 100);
window.addEventListener('scroll', throttledScroll);
// Polling with setInterval
function poll(fn, interval, maxAttempts) {
let attempts = 0;
const intervalId = setInterval(() => {
attempts++;
if (fn() || attempts >= maxAttempts) {
clearInterval(intervalId);
}
}, interval);
}
// Check if element exists
poll(() => {
const element = document.querySelector('.dynamic-element');
if (element) {
console.log('Element found!');
return true;
}
return false;
}, 100, 50); // Check every 100ms, max 50 times
// Sleep function
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage with async/await
async function example() {
console.log('Start');
await sleep(1000);
console.log('After 1 second');
await sleep(1000);
console.log('After 2 seconds');
}
// Timeout promise
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
// Race against timeout
async function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
timeout(ms)
]);
}
// Countdown timer
function countdown(seconds, callback) {
let remaining = seconds;
const intervalId = setInterval(() => {
console.log(`${remaining} seconds remaining`);
remaining--;
if (remaining < 0) {
clearInterval(intervalId);
callback();
}
}, 1000);
return intervalId; // Return for cancellation
}
countdown(5, () => console.log('Done!'));
// Animation loop comparison
// ❌ Bad: setInterval (not synced with screen refresh)
setInterval(() => {
updateAnimation();
}, 16); // Tries for 60fps but not precise
// ✓ Good: requestAnimationFrame (synced with refresh)
function animationLoop() {
updateAnimation();
requestAnimationFrame(animationLoop);
}
requestAnimationFrame(animationLoop);
// Background tab throttling
// Browsers throttle timers in background tabs to save resources
setTimeout(() => {
console.log('Might be delayed if tab inactive');
}, 1000);
// RAF stops in background tabs
requestAnimationFrame(() => {
console.log('Only runs when tab visible');
});
// Immediate execution alternative
// setTimeout(fn, 0) vs setImmediate (Node.js)
// vs queueMicrotask
setTimeout(() => console.log('setTimeout 0'), 0); // Macrotask
queueMicrotask(() => console.log('queueMicrotask')); // Microtask
// Output: queueMicrotask, setTimeout 0
// Cleanup pattern
class Component {
constructor() {
this.timers = new Set();
}
setTimeout(fn, delay) {
const id = setTimeout(() => {
this.timers.delete(id);
fn();
}, delay);
this.timers.add(id);
return id;
}
destroy() {
// Clear all timers
this.timers.forEach(id => clearTimeout(id));
this.timers.clear();
}
}
// Accurate interval with RAF
function setIntervalRAF(callback, interval) {
let lastTime = performance.now();
function loop(currentTime) {
const elapsed = currentTime - lastTime;
if (elapsed >= interval) {
lastTime = currentTime;
callback();
}
requestAnimationFrame(loop);
}
const id = requestAnimationFrame(loop);
return () => cancelAnimationFrame(id);
}
// Usage
const cancel = setIntervalRAF(() => {
console.log('Every 1000ms with RAF');
}, 1000);
// Later: cancel();
Warning:
setInterval can drift and overlap if callback takes longer than interval.
Background tabs throttle timers. Use requestAnimationFrame for animations. Minimum setTimeout delay
~4ms.
Section 14 Summary
- Callbacks: Error-first pattern
(err, result); handle errors inside async code; avoid callback hell (pyramid of doom); extract named functions; callbacks execute once - Promises: Three states (pending/fulfilled/rejected);
then/catch/finallychainable; return value wrapped in Promise; errors bubble to nearest catch;Promise.resolve/rejectcreate settled Promises - async/await:
asyncfunctions return Promise;awaitpauses until Promise settles; try/catch for error handling; sequential await blocks execution; use Promise.all for parallel - Combinators:
Promise.allfail-fast (all required);Promise.racefirst to settle;Promise.allSettlednever rejects;Promise.anyfirst to fulfill (ES2021) - Event Loop: Call stack → microtasks → render → one macrotask → repeat; microtasks drain completely before next macrotask; don't block event loop; split long tasks
- Micro/Macro: Microtasks: Promises, queueMicrotask (high priority); Macrotasks: setTimeout, setInterval (lower priority); all microtasks before next macrotask; can block rendering
- Timers:
setTimeoutonce;setIntervalrepeats (can drift);requestAnimationFramefor animations (~60fps); use recursive setTimeout for accuracy; background tabs throttled
15. Modern ES6+ Features and Syntax
15.1 Destructuring Assignment (array, object, nested)
| Type | Syntax | Description | Example |
|---|---|---|---|
| Array Destructuring | [a, b] = array |
Extract array elements into variables | [x, y] = [1, 2] |
| Object Destructuring | {a, b} = obj |
Extract object properties into variables | {name, age} = user |
| Skip Elements | [a, , c] = array |
Skip unwanted array elements | [x, , z] = [1, 2, 3] |
| Rest Elements | [a, ...rest] = array |
Collect remaining elements | [first, ...others] = [1,2,3] |
| Default Values | [a = 0] = array |
Fallback if undefined | {name = 'Guest'} = obj |
| Rename Properties | {a: newName} = obj |
Assign to different variable name | {name: userName} = obj |
| Nested Destructuring | {a: {b}} = obj |
Extract from nested structures | {user: {name}} = data |
| Mixed Destructuring | {a, b: [c, d]} = obj |
Combine object and array destructuring | {items: [first]} = obj |
Example: Destructuring patterns
// Array destructuring
const arr = [1, 2, 3, 4, 5];
const [first, second] = arr;
console.log(first, second); // 1, 2
const [a, b, c] = arr;
console.log(a, b, c); // 1, 2, 3
// Skip elements
const [x, , z] = arr;
console.log(x, z); // 1, 3
// Rest elements (...) must be last
const [head, ...tail] = arr;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Default values
const [p = 10, q = 20] = [5];
console.log(p, q); // 5, 20
// Swap variables
let m = 1, n = 2;
[m, n] = [n, m];
console.log(m, n); // 2, 1
// Object destructuring
const user = {name: 'Alice', age: 30, city: 'NYC'};
const {name, age} = user;
console.log(name, age); // 'Alice', 30
// Rename variables
const {name: userName, age: userAge} = user;
console.log(userName, userAge); // 'Alice', 30
// Default values
const {name, country = 'USA'} = user;
console.log(country); // 'USA' (not in user object)
// Rest properties
const {name, ...rest} = user;
console.log(rest); // {age: 30, city: 'NYC'}
// Nested destructuring
const data = {
user: {
name: 'Bob',
address: {
city: 'LA',
zip: '90001'
}
},
items: [1, 2, 3]
};
const {user: {name, address: {city}}} = data;
console.log(name, city); // 'Bob', 'LA'
// Array within object
const {items: [firstItem, secondItem]} = data;
console.log(firstItem, secondItem); // 1, 2
// Object within array
const users = [
{name: 'Alice', age: 30},
{name: 'Bob', age: 25}
];
const [{name: firstName}, {age: secondAge}] = users;
console.log(firstName, secondAge); // 'Alice', 25
// Function parameters
function greet({name, age = 18}) {
console.log(`${name} is ${age}`);
}
greet({name: 'Alice', age: 30}); // Alice is 30
greet({name: 'Bob'}); // Bob is 18
// Array parameters
function sum([a, b, c = 0]) {
return a + b + c;
}
sum([1, 2, 3]); // 6
sum([1, 2]); // 3 (c defaults to 0)
// Computed property names
const key = 'name';
const {[key]: value} = {name: 'Alice'};
console.log(value); // 'Alice'
// Destructuring in loops
const points = [
{x: 10, y: 20},
{x: 30, y: 40}
];
for (const {x, y} of points) {
console.log(x, y);
}
// Destructuring returned arrays
function getCoords() {
return [10, 20];
}
const [xCoord, yCoord] = getCoords();
// Destructuring returned objects
function getUser() {
return {name: 'Alice', age: 30};
}
const {name: n, age: a} = getUser();
// Mixed destructuring with defaults
const config = {
host: 'localhost',
port: 8080,
options: {
timeout: 3000,
retries: 3
}
};
const {
host = '0.0.0.0',
port = 80,
options: {
timeout = 5000,
retries = 5,
cache = true
} = {}
} = config;
// Destructuring with rest and rename
const person = {
firstName: 'John',
lastName: 'Doe',
age: 30,
city: 'NYC'
};
const {
firstName: fName,
lastName: lName,
...details
} = person;
console.log(fName, lName); // 'John', 'Doe'
console.log(details); // {age: 30, city: 'NYC'}
// Ignoring values
const [, , third] = [1, 2, 3, 4];
console.log(third); // 3
// Destructuring with existing variables
let existing1, existing2;
[existing1, existing2] = [10, 20];
// Must use parentheses for objects
let e1, e2;
({e1, e2} = {e1: 10, e2: 20}); // Parens required!
// Failed matches = undefined
const {nonExistent} = {};
console.log(nonExistent); // undefined
const [missing] = [];
console.log(missing); // undefined
// Practical: extracting data from API response
const response = {
data: {
user: {
id: 123,
profile: {
name: 'Alice',
email: 'alice@example.com'
}
}
},
meta: {
timestamp: Date.now()
}
};
const {
data: {
user: {
id: userId,
profile: {name: displayName, email}
}
}
} = response;
console.log(userId, displayName, email);
Note: Destructuring works with any iterable (arrays, strings, Maps, Sets). Use
...
for rest (must be last). Defaults only apply for undefined (not null). Object
destructuring needs parentheses for existing variables.
15.2 Spread and Rest Syntax Applications
| Context | Operator | Purpose | Example |
|---|---|---|---|
| Spread (Arrays) | [...arr] |
Expand array elements | [...arr1, ...arr2] |
| Spread (Objects) | {...obj} |
Expand object properties | {...obj1, ...obj2} |
| Spread (Function Calls) | fn(...args) |
Pass array as individual arguments | Math.max(...numbers) |
| Rest (Parameters) | (...args) |
Collect remaining arguments into array | function f(...args) |
| Rest (Destructuring) | [a, ...rest] |
Collect remaining elements/properties | {x, ...rest} = obj |
Example: Spread and rest usage
// Spread in array literals
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
const withMiddle = [...arr1, 'middle', ...arr2];
console.log(withMiddle); // [1, 2, 3, 'middle', 4, 5, 6]
// Array cloning (shallow)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4); // Doesn't affect original
// Spread strings
const str = 'hello';
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
// Spread with Sets (removes duplicates)
const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]
// Spread in function calls
const numbers = [5, 1, 9, 3, 7];
Math.max(...numbers); // 9 (instead of Math.max(5, 1, 9, 3, 7))
console.log(...numbers); // 5 1 9 3 7
// Spread objects (shallow merge)
const obj1 = {a: 1, b: 2};
const obj2 = {b: 3, c: 4};
const merged = {...obj1, ...obj2};
console.log(merged); // {a: 1, b: 3, c: 4} (obj2.b overwrites obj1.b)
// Adding/overriding properties
const user = {name: 'Alice', age: 30};
const updated = {...user, age: 31, city: 'NYC'};
console.log(updated); // {name: 'Alice', age: 31, city: 'NYC'}
// Object cloning (shallow)
const originalObj = {a: 1, b: {c: 2}};
const clonedObj = {...originalObj};
clonedObj.b.c = 3; // Affects original! (nested objects not cloned)
// Rest parameters (collect into array)
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
// Rest must be last parameter
function logAll(first, second, ...rest) {
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
}
logAll(1, 2, 3, 4, 5);
// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
const {x, y, ...rest} = {x: 1, y: 2, z: 3, w: 4};
console.log(x, y); // 1, 2
console.log(rest); // {z: 3, w: 4}
// Combining spread and rest
function multiply(multiplier, ...numbers) {
return numbers.map(n => n * multiplier);
}
const nums = [1, 2, 3];
multiply(2, ...nums); // [2, 4, 6]
// Converting arguments to array (old way vs new)
function oldWay() {
const args = Array.prototype.slice.call(arguments);
return args;
}
function newWay(...args) {
return args;
}
// Spread with iterables
const map = new Map([['a', 1], ['b', 2]]);
const mapArr = [...map];
console.log(mapArr); // [['a', 1], ['b', 2]]
const set = new Set([1, 2, 3]);
const setArr = [...set];
console.log(setArr); // [1, 2, 3]
// Practical: remove item from array
const items = [1, 2, 3, 4, 5];
const index = 2;
const removed = [...items.slice(0, index), ...items.slice(index + 1)];
console.log(removed); // [1, 2, 4, 5]
// Practical: insert item
const insert = [...items.slice(0, index), 99, ...items.slice(index)];
console.log(insert); // [1, 2, 99, 3, 4, 5]
// Practical: conditional spread
const baseConfig = {host: 'localhost', port: 8080};
const isDev = true;
const config = {
...baseConfig,
...(isDev && {debug: true, verbose: true})
};
// If isDev: {host: 'localhost', port: 8080, debug: true, verbose: true}
// If !isDev: {host: 'localhost', port: 8080}
// Practical: merge with priority
function mergeOptions(defaults, user) {
return {...defaults, ...user};
}
const options = mergeOptions(
{timeout: 5000, retries: 3},
{retries: 5} // Overrides defaults.retries
);
// Practical: flatten array one level
const nested = [[1, 2], [3, 4], [5]];
const flat = [].concat(...nested);
console.log(flat); // [1, 2, 3, 4, 5]
// Better: use flat()
const flatBetter = nested.flat();
// Spread with null/undefined (error)
// const spread = {...null}; // TypeError
// const spread = {...undefined}; // TypeError
// But arrays are OK
const arr = [...(someVar || [])]; // Safe fallback
// Order matters in objects
const a = {x: 1, y: 2};
const b = {y: 3, z: 4};
{...a, ...b}; // {x: 1, y: 3, z: 4} (b.y wins)
{...b, ...a}; // {x: 1, y: 2, z: 4} (a.y wins)
// Spread doesn't deep clone
const obj = {
a: 1,
nested: {b: 2}
};
const clone = {...obj};
clone.nested.b = 3; // Affects original!
console.log(obj.nested.b); // 3
// Deep clone requires recursion or library
const deepClone = JSON.parse(JSON.stringify(obj)); // Quick but limited
// Rest with default parameters
function greet(greeting = 'Hello', ...names) {
return `${greeting} ${names.join(' and ')}`;
}
greet('Hi', 'Alice', 'Bob'); // "Hi Alice and Bob"
greet(undefined, 'Alice'); // "Hello Alice"
Warning: Spread/rest creates shallow copies (nested
objects/arrays still referenced). Rest must be last parameter/element. Spread objects only copies enumerable own
properties. Can't spread null/undefined.
15.3 Default Parameters and Parameter Destructuring
| Feature | Syntax | Description | Example |
|---|---|---|---|
| Default Value | function f(a = default) |
Fallback if undefined (not null) | f(x = 0) |
| Expression Default | function f(a = expr()) |
Default can be expression; evaluated when needed | f(x = Date.now()) |
| Reference Previous | function f(a, b = a) |
Later params can reference earlier ones | f(x, y = x * 2) |
| Destructured Params | function f({x, y}) |
Destructure object/array parameter | f({x: 1, y: 2}) |
| Destructure + Default | function f({x = 0}) |
Defaults within destructuring | f({x: 1} = {}) |
Example: Default parameters
// Basic default parameters
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
greet('Alice'); // "Hello, Alice!"
greet(); // "Hello, Guest!"
greet(undefined); // "Hello, Guest!"
greet(null); // "Hello, null!" (null is NOT undefined!)
// Multiple defaults
function createUser(name = 'Anonymous', age = 18, active = true) {
return {name, age, active};
}
createUser('Alice', 30); // {name: 'Alice', age: 30, active: true}
createUser('Bob'); // {name: 'Bob', age: 18, active: true}
// Expression as default (evaluated each call)
function log(message, timestamp = Date.now()) {
console.log(`[${timestamp}] ${message}`);
}
log('First'); // [1702812345123] First
log('Second'); // [1702812345456] Second (different timestamp)
// Function call as default
function getDefault() {
console.log('Getting default');
return 42;
}
function withDefault(x = getDefault()) {
return x;
}
withDefault(10); // 10 (getDefault not called)
withDefault(); // 42 (getDefault called, logs "Getting default")
// Reference previous parameters
function createRange(start = 0, end = start + 10) {
return {start, end};
}
createRange(5); // {start: 5, end: 15}
createRange(); // {start: 0, end: 10}
// Parameters evaluated left-to-right
function multiply(a, b = a, c = a * b) {
return c;
}
multiply(2); // 4 (b=2, c=2*2)
multiply(2, 3); // 6 (c=2*3)
multiply(2, 3, 4); // 4 (c explicitly set)
// Can't reference later parameters
function invalid(a = b, b = 1) { // ReferenceError!
return a;
}
// Object destructuring parameters
function drawCircle({x, y, radius, color = 'black'}) {
console.log(`Circle at (${x}, ${y}), r=${radius}, color=${color}`);
}
drawCircle({x: 10, y: 20, radius: 5});
// "Circle at (10, 20), r=5, color=black"
drawCircle({x: 0, y: 0, radius: 10, color: 'red'});
// "Circle at (0, 0), r=10, color=red"
// Default for entire parameter object
function config({host = 'localhost', port = 8080} = {}) {
return {host, port};
}
config({host: '0.0.0.0'}); // {host: '0.0.0.0', port: 8080}
config(); // {host: 'localhost', port: 8080}
// Array destructuring parameters
function sum([a = 0, b = 0, c = 0]) {
return a + b + c;
}
sum([1, 2, 3]); // 6
sum([1, 2]); // 3 (c defaults to 0)
sum([1]); // 1 (b and c default to 0)
// Nested destructuring with defaults
function createUser({
name = 'Anonymous',
profile: {
age = 18,
email = 'no-email'
} = {}
} = {}) {
return {name, age, email};
}
createUser({name: 'Alice', profile: {age: 30}});
// {name: 'Alice', age: 30, email: 'no-email'}
createUser({name: 'Bob'});
// {name: 'Bob', age: 18, email: 'no-email'}
createUser();
// {name: 'Anonymous', age: 18, email: 'no-email'}
// Rest with defaults
function logAll(first = 'default', ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
}
logAll(); // First: default, Rest: []
logAll('a', 'b', 'c'); // First: a, Rest: ['b', 'c']
// Complex example: options object
function ajax(url, {
method = 'GET',
headers = {},
body = null,
timeout = 5000,
retries = 3
} = {}) {
console.log({url, method, headers, body, timeout, retries});
}
ajax('/api/data'); // All defaults
ajax('/api/data', {method: 'POST', body: '{}'});
// Default in arrow functions
const greet = (name = 'Guest') => `Hello, ${name}!`;
// Destructuring in arrow functions
const getArea = ({width = 0, height = 0}) => width * height;
getArea({width: 10, height: 20}); // 200
getArea({width: 10}); // 0 (height defaults to 0)
// Optional destructuring
function processData(data = {}) {
const {name = 'Unknown', value = 0} = data;
return {name, value};
}
processData(); // {name: 'Unknown', value: 0}
processData({name: 'Test'}); // {name: 'Test', value: 0}
// Default with computed properties
const key = 'port';
function server({[key]: p = 8080} = {}) {
return p;
}
server({port: 3000}); // 3000
server(); // 8080
// Avoiding undefined vs null confusion
function safe(value = 'default') {
return value ?? 'fallback'; // Use nullish coalescing
}
safe(); // 'default' (undefined)
safe(undefined); // 'default'
safe(null); // 'fallback' (null not undefined)
safe(0); // 0
safe(''); // ''
// Practical: API request options
function fetchData(url, {
method = 'GET',
cache = true,
retries = 3,
onSuccess = () => {},
onError = console.error
} = {}) {
// Implementation
}
Note: Defaults only apply when parameter is
undefined (not null,
0, '', etc). Expressions evaluated on each call. Can reference previous params. Use
= {} for destructured param defaults.
15.4 Enhanced Object Literals and Method Shorthand
| Feature | ES6 Syntax | ES5 Equivalent | Description |
|---|---|---|---|
| Property Shorthand | {name, age} |
{name: name, age: age} |
Variable name as property name |
| Method Shorthand | {method() {}} |
{method: function() {}} |
Concise method syntax |
| Computed Properties | {[expr]: value} |
obj[expr] = value (after creation) | Dynamic property names |
| Computed Methods | {[expr]() {}} |
obj[expr] = function() {} | Dynamic method names |
| Getter/Setter | {get prop() {}, set prop(v) {}} |
Object.defineProperty | Accessor properties |
Example: Enhanced object literals
// Property shorthand
const name = 'Alice';
const age = 30;
// ES6
const user = {name, age};
// ES5 equivalent
const userOld = {name: name, age: age};
console.log(user); // {name: 'Alice', age: 30}
// Method shorthand
const calculator = {
// ES6
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
// ES5 equivalent
multiply: function(a, b) {
return a * b;
}
};
calculator.add(5, 3); // 8
// Computed property names
const prop = 'dynamicKey';
const value = 42;
const obj = {
[prop]: value,
['key' + 2]: 'value2',
[`computed_${Date.now()}`]: 'timestamp'
};
console.log(obj.dynamicKey); // 42
console.log(obj.key2); // 'value2'
// Computed method names
const methodName = 'sayHello';
const greeter = {
[methodName]() {
return 'Hello!';
},
[`${methodName}Loud`]() {
return 'HELLO!';
}
};
greeter.sayHello(); // 'Hello!'
greeter.sayHelloLoud(); // 'HELLO!'
// Combining features
const id = 1;
const name = 'Product';
const price = 99.99;
const product = {
id,
name,
price,
// Method shorthand
getInfo() {
return `${this.name}: ${this.price}`;
},
// Computed property
[`item_${id}`]: true,
// Getter
get displayPrice() {
return `${this.price.toFixed(2)}`;
},
// Setter
set discount(percent) {
this.price = this.price * (1 - percent / 100);
}
};
product.getInfo(); // "Product: $99.99"
product.displayPrice; // "$99.99"
product.discount = 10; // Sets price to 89.99
// Getters and setters
const person = {
firstName: 'John',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
};
person.fullName; // "John Doe"
person.fullName = 'Jane Smith';
console.log(person.firstName); // "Jane"
// Async methods
const api = {
async fetchData() {
const response = await fetch('/api/data');
return response.json();
},
async* generateData() {
for (let i = 0; i < 5; i++) {
yield await fetchItem(i);
}
}
};
// Generator methods
const iterator = {
*range(start, end) {
for (let i = start; i < end; i++) {
yield i;
}
}
};
for (const num of iterator.range(1, 5)) {
console.log(num); // 1, 2, 3, 4
}
// Computed properties with symbols
const MY_KEY = Symbol('myKey');
const obj = {
[MY_KEY]: 'hidden value',
public: 'visible'
};
obj[MY_KEY]; // 'hidden value'
// Dynamic object building
function createUser(name, age, ...permissions) {
return {
name,
age,
permissions,
hasPermission(perm) {
return this.permissions.includes(perm);
},
[`is${name}`]: true
};
}
const user = createUser('Admin', 30, 'read', 'write');
user.isAdmin; // true
user.hasPermission('read'); // true
// Mixing with spread
const defaults = {
timeout: 5000,
retries: 3
};
const config = {
...defaults,
host: 'localhost',
port: 8080,
getUrl() {
return `http://${this.host}:${this.port}`;
}
};
// Nested enhanced literals
const data = {
user: {
name,
age,
getInfo() {
return `${this.name}, ${this.age}`;
}
},
metadata: {
created: Date.now(),
format() {
return new Date(this.created).toISOString();
}
}
};
// __proto__ property (not recommended)
const parent = {
greet() {
return 'Hello from parent';
}
};
const child = {
__proto__: parent,
greet() {
return super.greet() + ' and child';
}
};
child.greet(); // "Hello from parent and child"
// Practical: factory function
function createPoint(x, y) {
return {
x,
y,
distanceFrom(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
},
toString() {
return `(${this.x}, ${this.y})`;
}
};
}
const p1 = createPoint(0, 0);
const p2 = createPoint(3, 4);
p1.distanceFrom(p2); // 5
// Practical: configuration object
const env = 'production';
const appConfig = {
env,
get isProduction() {
return this.env === 'production';
},
get isDevelopment() {
return this.env === 'development';
},
[`${env}_url`]: 'https://api.example.com',
log(message) {
if (this.isDevelopment) {
console.log(message);
}
}
};
Note: Property shorthand uses variable name as key. Method shorthand omits
: function. Computed properties use [expression]. Getters/setters accessed like
properties. Methods can be async, generators, or both.
15.5 Template Literals and Tagged Templates
| Feature | Syntax | Description | Example |
|---|---|---|---|
| Basic Template | `text` |
Backtick-delimited string | `Hello` |
| Interpolation | `text ${expr}` |
Embed expression result | `x = ${x}` |
| Multi-line | `line1<br>line2` |
Preserve newlines | `First<br>Second` |
| Tagged Template | tag`text ${expr}` |
Function processes template | html`<div>${content}</div>` |
| Raw Strings | String.raw`text` |
Get raw string (no escape processing) | String.raw`C:\temp` |
Example: Template literals
// Basic template literals
const name = 'Alice';
const greeting = `Hello, ${name}!`;
console.log(greeting); // "Hello, Alice!"
// Expression interpolation
const a = 10;
const b = 20;
console.log(`${a} + ${b} = ${a + b}`); // "10 + 20 = 30"
// Any expression
const price = 99.99;
console.log(`Price: ${price.toFixed(2)}`); // "Price: $99.99"
// Function calls
function upper(str) {
return str.toUpperCase();
}
console.log(`Name: ${upper(name)}`); // "Name: ALICE"
// Multi-line strings
const html = `
<div>
<h1>Title</h1>
<p>Content</p>
</div>
`;
// Preserves indentation and newlines
// Old way (ES5)
const oldHtml =
'<div>\n' +
' <h1>Title</h1>\n' +
' <p>Content</p>\n' +
'</div>';
// Nested templates
const user = {name: 'Bob', age: 30};
const info = `User: ${user.name} (${user.age > 18 ? 'Adult' : 'Minor'})`;
// Escaping backticks
const text = `This is a backtick: \``;
// Tagged templates (custom processing)
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] || '';
return result + str + `<mark>${value}</mark>`;
}, '');
}
const score = 95;
const result = highlight`Your score is ${score}%`;
// "Your score is <mark>95</mark>%"
// Tagged template: SQL escaping (example)
function sql(strings, ...values) {
return strings.reduce((query, str, i) => {
const value = values[i];
const escaped = typeof value === 'string'
? value.replace(/'/g, "''")
: value;
return query + str + (escaped !== undefined ? escaped : '');
}, '');
}
const userId = 123;
const userName = "O'Brien";
const query = sql`SELECT * FROM users WHERE id = ${userId} AND name = '${userName}'`;
// "SELECT * FROM users WHERE id = 123 AND name = 'O''Brien'"
// Tagged template: HTML escaping
function html(strings, ...values) {
const escape = (str) => String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
return strings.reduce((result, str, i) => {
const value = values[i] !== undefined ? escape(values[i]) : '';
return result + str + value;
}, '');
}
const userInput = '<script>alert("XSS")</script>';
const safe = html`<div>${userInput}</div>`;
// "<div><script>alert("XSS")</script></div>"
// String.raw (no escape processing)
const path = String.raw`C:\Users\Documents\file.txt`;
console.log(path); // "C:\Users\Documents\file.txt" (backslashes preserved)
// Normal template would process escapes
const normalPath = `C:\Users\Documents\file.txt`;
// Escape sequences like \U, \D, \f would be interpreted
// Raw string with interpolation
const dir = 'Documents';
const rawPath = String.raw`C:\${dir}\file.txt`;
console.log(rawPath); // "C:\Documents\file.txt"
// Access raw strings in tagged templates
function showRaw(strings, ...values) {
console.log('Cooked:', strings);
console.log('Raw:', strings.raw);
console.log('Values:', values);
}
showRaw`Line 1\nLine 2 ${42}`;
// Cooked: ['Line 1
// Line 2 ', '']
// Raw: ['Line 1\\nLine 2 ', '']
// Values: [42]
// Practical: styled components (React)
function css(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] || '');
}, '');
}
const Button = css`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
`;
// Practical: i18n (internationalization)
function i18n(strings, ...values) {
const key = strings.join('{}');
// Look up translation for key
return translate(key, values);
}
const count = 5;
const message = i18n`You have ${count} new messages`;
// Practical: GraphQL queries
const gql = (strings, ...values) => strings.join('');
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
`;
// Practical: conditional parts
const showDetails = true;
const card = `
<div class="card">
<h3>${name}</h3>
${showDetails ? `<p>Age: ${age}</p>` : ''}
</div>
`;
// Practical: array to list
const items = ['Apple', 'Banana', 'Cherry'];
const list = `
<ul>
${items.map(item => `<li>${item}</li>`).join('\n ')}
</ul>
`;
// Expression can be any valid JavaScript
console.log(`Random: ${Math.random()}`);
console.log(`Array length: ${[1, 2, 3].length}`);
console.log(`Object: ${JSON.stringify({a: 1})}`);
// Template in template
const inner = `inner text`;
const outer = `outer ${`nested ${inner}`} text`;
// Empty interpolation
const empty = `Value: ${''}`;
console.log(empty); // "Value: "
// Undefined/null
console.log(`Value: ${undefined}`); // "Value: undefined"
console.log(`Value: ${null}`); // "Value: null"
Note: Template literals use backticks (`). Interpolate with
${expression}.
Preserve newlines/indentation. Tagged templates: function(strings, ...values) where
strings.raw has unprocessed escapes. Expressions converted to strings.
15.6 Symbol Type and Symbol Registry
| Method/Property | Syntax | Description | Returns |
|---|---|---|---|
| Symbol() | Symbol([description]) |
Create unique symbol; optional description | Unique symbol value |
| Symbol.for() | Symbol.for(key) |
Get/create global symbol by key | Symbol (reuses if exists) |
| Symbol.keyFor() | Symbol.keyFor(sym) |
Get key for global symbol | String key or undefined |
| description | symbol.description |
Get symbol description | String or undefined |
| typeof | typeof sym |
Type check | 'symbol' |
Well-Known Symbols
| Symbol | Purpose | Used By |
|---|---|---|
| Symbol.iterator | Define default iterator | for...of, spread, destructuring |
| Symbol.asyncIterator | Define async iterator | for await...of |
| Symbol.toStringTag | Customize Object.prototype.toString | Object.prototype.toString.call() |
| Symbol.toPrimitive | Convert object to primitive | Type coercion |
| Symbol.hasInstance | Customize instanceof behavior | instanceof operator |
Example: Symbols
// Create unique symbols
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false (always unique)
// Symbols with descriptions
const id = Symbol('id');
const userId = Symbol('id');
console.log(id === userId); // false (still unique despite same description)
// Description property
console.log(id.description); // "id"
// typeof
console.log(typeof id); // "symbol"
// Symbols as object keys (hidden from normal iteration)
const obj = {
name: 'Alice',
[id]: 123
};
console.log(obj[id]); // 123
console.log(obj.name); // 'Alice'
// Not enumerable in for...in
for (let key in obj) {
console.log(key); // Only logs "name" (not id)
}
// Not in Object.keys()
console.log(Object.keys(obj)); // ['name']
// Symbols visible with Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id)]
// Or Reflect.ownKeys() (all keys)
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]
// Global symbol registry (shared across realms)
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true (same key = same symbol)
// Get key for global symbol
console.log(Symbol.keyFor(globalSym1)); // "app.id"
// Local symbols have no key
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined
// Well-known symbols: Symbol.iterator
const iterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return {value: data[index++], done: false};
}
return {done: true};
}
};
}
};
for (const value of iterable) {
console.log(value); // 1, 2, 3
}
// Symbol.toStringTag (customize toString)
class MyClass {
get [Symbol.toStringTag]() {
return 'MyClass';
}
}
const instance = new MyClass();
console.log(Object.prototype.toString.call(instance)); // "[object MyClass]"
// Symbol.toPrimitive (customize type conversion)
const obj2 = {
value: 100,
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.value;
case 'string':
return `${this.value}`;
case 'default':
return this.value;
default:
throw new Error('Invalid hint');
}
}
};
console.log(+obj2); // 100 (number hint)
console.log(`${obj2}`); // "$100" (string hint)
console.log(obj2 + 50); // 150 (default hint)
// Symbol.hasInstance (customize instanceof)
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([1, 2, 3] instanceof MyArray); // true
// Private object properties (convention)
const _internal = Symbol('internal');
class Counter {
constructor() {
this[_internal] = 0;
}
increment() {
this[_internal]++;
}
getValue() {
return this[_internal];
}
}
const counter = new Counter();
counter.increment();
console.log(counter.getValue()); // 1
console.log(counter[_internal]); // undefined (no access to local symbol)
// But symbols are not truly private
const symbols = Object.getOwnPropertySymbols(counter);
console.log(counter[symbols[0]]); // 1 (can still access)
// Symbols in JSON (ignored)
const data = {
name: 'Alice',
[Symbol('secret')]: 'hidden'
};
console.log(JSON.stringify(data)); // {"name":"Alice"}
// Cannot convert symbol to string directly
const sym = Symbol('test');
// String(sym); // TypeError
// "" + sym; // TypeError
sym.toString(); // "Symbol(test)" (explicit toString OK)
// Symbols in Maps/Sets
const map = new Map();
const key = Symbol('key');
map.set(key, 'value');
console.log(map.get(key)); // "value"
// Practical: unique constants
const STATUS = {
PENDING: Symbol('pending'),
APPROVED: Symbol('approved'),
REJECTED: Symbol('rejected')
};
let orderStatus = STATUS.PENDING;
if (orderStatus === STATUS.PENDING) {
console.log('Order is pending');
}
// Practical: metadata keys (avoid collisions)
const METADATA_KEY = Symbol.for('app.metadata');
function addMetadata(obj, data) {
obj[METADATA_KEY] = data;
}
function getMetadata(obj) {
return obj[METADATA_KEY];
}
const user = {name: 'Alice'};
addMetadata(user, {created: Date.now()});
// Practical: protocol/interface check
const SERIALIZABLE = Symbol.for('serializable');
class User {
[SERIALIZABLE]() {
return JSON.stringify(this);
}
}
function serialize(obj) {
if (SERIALIZABLE in obj) {
return obj[SERIALIZABLE]();
}
throw new Error('Not serializable');
}
// Symbol.match, Symbol.replace, etc. (regex methods)
class MyMatcher {
[Symbol.match](str) {
return str.includes('test') ? ['test'] : null;
}
}
const matcher = new MyMatcher();
console.log('testing'.match(matcher)); // ['test']
Warning: Symbols are unique (except global registry via
Symbol.for()). Not
enumerable in for...in or Object.keys(), but visible with
Object.getOwnPropertySymbols(). Not truly private. Can't convert to string with + or
''.
15.7 WeakMap and WeakSet Collections
| Collection | Key Type | Values | GC Behavior |
|---|---|---|---|
| WeakMap | Objects only | Any type | Keys weakly held; GC if no other references |
| WeakSet | Objects only | Objects (values are keys) | Values weakly held; GC if no other references |
WeakMap Methods
| Method | Syntax | Description | Returns |
|---|---|---|---|
| set() | weakMap.set(key, value) |
Add/update key-value pair | WeakMap instance |
| get() | weakMap.get(key) |
Get value for key | Value or undefined |
| has() | weakMap.has(key) |
Check if key exists | Boolean |
| delete() | weakMap.delete(key) |
Remove key-value pair | Boolean (true if deleted) |
WeakSet Methods
| Method | Syntax | Description | Returns |
|---|---|---|---|
| add() | weakSet.add(value) |
Add object to set | WeakSet instance |
| has() | weakSet.has(value) |
Check if value exists | Boolean |
| delete() | weakSet.delete(value) |
Remove value from set | Boolean (true if deleted) |
Example: WeakMap and WeakSet
// WeakMap basics
const weakMap = new WeakMap();
const obj1 = {name: 'Alice'};
const obj2 = {name: 'Bob'};
// Set values (keys must be objects)
weakMap.set(obj1, 'data for obj1');
weakMap.set(obj2, 'data for obj2');
// Get values
console.log(weakMap.get(obj1)); // "data for obj1"
// Check existence
console.log(weakMap.has(obj2)); // true
// Delete
weakMap.delete(obj1);
console.log(weakMap.has(obj1)); // false
// Primitives not allowed as keys
// weakMap.set('string', 'value'); // TypeError
// weakMap.set(123, 'value'); // TypeError
// weakMap.set(Symbol(), 'value'); // TypeError
// Garbage collection example
let user = {name: 'Alice'};
weakMap.set(user, {loginCount: 5});
user = null; // No more references to object
// Object and its WeakMap entry can be garbage collected
// No size property
console.log(weakMap.size); // undefined
// Not iterable
// for (let entry of weakMap) {} // TypeError
// weakMap.forEach() // undefined (no method)
// WeakSet basics
const weakSet = new WeakSet();
const item1 = {id: 1};
const item2 = {id: 2};
// Add objects
weakSet.add(item1);
weakSet.add(item2);
// Check membership
console.log(weakSet.has(item1)); // true
// Delete
weakSet.delete(item1);
console.log(weakSet.has(item1)); // false
// Primitives not allowed
// weakSet.add('string'); // TypeError
// weakSet.add(123); // TypeError
// Practical: Private data for objects
const privateData = new WeakMap();
class Person {
constructor(name, ssn) {
this.name = name; // Public
privateData.set(this, {ssn}); // Private
}
getSSN() {
return privateData.get(this).ssn;
}
}
const person = new Person('Alice', '123-45-6789');
console.log(person.name); // "Alice"
console.log(person.getSSN()); // "123-45-6789"
console.log(person.ssn); // undefined (not accessible)
// When person is garbage collected, privateData entry is too
// Practical: DOM node metadata
const nodeMetadata = new WeakMap();
function trackNode(node, metadata) {
nodeMetadata.set(node, metadata);
}
function getNodeData(node) {
return nodeMetadata.get(node);
}
const div = document.createElement('div');
trackNode(div, {clicks: 0, created: Date.now()});
// When div is removed from DOM and no references exist, GC cleans up
// Practical: Cache/memoization
const cache = new WeakMap();
function expensiveOperation(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
// Expensive computation
const result = obj.value * 2;
cache.set(obj, result);
return result;
}
const data = {value: 10};
expensiveOperation(data); // Computes and caches
expensiveOperation(data); // Returns cached result
// When data is no longer referenced, cache entry is GC'd
// Practical: Object flagging with WeakSet
const processedObjects = new WeakSet();
function processObject(obj) {
if (processedObjects.has(obj)) {
console.log('Already processed');
return;
}
// Process object
console.log('Processing', obj);
processedObjects.add(obj);
}
const obj = {id: 1};
processObject(obj); // "Processing {id: 1}"
processObject(obj); // "Already processed"
// When obj is GC'd, WeakSet entry is too
// Practical: Preventing circular reference leaks
class Node {
constructor(value) {
this.value = value;
this.children = [];
}
addChild(node) {
this.children.push(node);
}
}
// Track parent-child relationships without preventing GC
const parentMap = new WeakMap();
function setParent(child, parent) {
parentMap.set(child, parent);
}
function getParent(child) {
return parentMap.get(child);
}
const parent = new Node('parent');
const child = new Node('child');
parent.addChild(child);
setParent(child, parent);
// WeakMap vs Map comparison
const regularMap = new Map();
const weakMap2 = new WeakMap();
let key = {id: 1};
regularMap.set(key, 'value');
weakMap2.set(key, 'value');
key = null;
// regularMap still holds reference to object (prevents GC)
// weakMap2 does not hold reference (allows GC)
// Use cases for WeakMap:
// 1. Private instance data
// 2. Metadata for objects
// 3. Caching results for objects
// 4. DOM node annotations
// 5. Prevent memory leaks with object associations
// Use cases for WeakSet:
// 1. Track processed objects
// 2. Mark objects without modifying them
// 3. Prevent duplicate processing
// 4. Object flags/tags
// Limitations:
// - Keys/values must be objects (no primitives)
// - Not enumerable (can't iterate)
// - No size property
// - No clear() method
// - Can't get all keys/values
// Why WeakMap instead of Map with cleanup?
// - Automatic garbage collection
// - No manual memory management needed
// - Prevents memory leaks
// - Cleaner code (no cleanup logic)
// When NOT to use WeakMap/WeakSet:
// - Need to iterate over entries
// - Need to know collection size
// - Keys/values are primitives
// - Need to serialize/deserialize
// - Need persistent storage
// WeakMap with object literals (careful!)
weakMap.set({}, 'value'); // No reference = immediately GC'd!
console.log(weakMap.get({})); // undefined (different object)
// Must keep reference
const key2 = {};
weakMap.set(key2, 'value');
console.log(weakMap.get(key2)); // "value"
Note: WeakMap/WeakSet keys/values must be objects (no
primitives). Entries automatically garbage collected when no other references exist. Not enumerable (no size,
forEach, keys, values, entries). Use for private data, metadata, caching without memory leaks.
Section 15 Summary:
- Destructuring: Extract array/object values with
[a, b] = arror{x, y} = obj; supports defaults, rest, skip, rename, nesting; works in parameters - Spread: Expand arrays/objects with
...in literals/calls ([...arr],{...obj}); shallow copy; merge collections; spread strings/iterables - Rest: Collect remaining items with
...rest(must be last); gather function arguments into array; collect remaining properties in destructuring - Defaults: Parameter fallbacks with
= value; only forundefined(notnull); expressions evaluated each call; can reference previous parameters - Object Literals: Property shorthand
{x, y}; method shorthandmethod() {}; computed keys[expr]; getters/setters; can be async/generator - Templates: Backtick strings with interpolation
`text ${expr}`; multi-line; tagged templatestag`...`for custom processing;String.rawfor raw strings - Symbols: Unique immutable values via
Symbol(); global registrySymbol.for(key); hidden from normal enumeration; well-known symbols customize behavior (iterator, toStringTag, toPrimitive) - WeakMap/WeakSet: Collections with object-only keys/values; weak references (allow GC); no size/iteration; use for private data, metadata, caching without memory leaks
16. ES2015+ Advanced Features
16.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.
16.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()}.
16.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.
16.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.
16.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.
16.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.
16.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)
17. Classes and Object-Oriented Programming
17.1 Class Declaration and Constructor Methods
Class Declaration Syntax
| Feature | Syntax | Description |
|---|---|---|
| Basic Class | class Name {} |
Class declaration (not hoisted) |
| Constructor | constructor(params) {} |
Initialize instance (only one per class) |
| Instance Method | method() {} |
Method on prototype |
| Static Method | static method() {} |
Method on class itself |
| Field Declaration | field = value; |
Public instance field |
| Static Field | static field = value; |
Static field on class |
| Private Field | #field = value; |
Private instance field |
| Getter | get prop() {} |
Property getter accessor |
| Setter | set prop(val) {} |
Property setter accessor |
Constructor Features
| Feature | Description | Behavior |
|---|---|---|
| Return Value | Implicit return of this | Explicit object return overrides instance |
| Super Call | Must call super() in derived class | Before accessing this in derived constructor |
| Parameters | Support all parameter types | Default, rest, destructuring parameters |
| Omitting Constructor | Default constructor created | Base: constructor() {}; Derived: super(...args) |
| Multiple Constructors | Not supported | Use factory methods or optional parameters |
Example: Basic class declaration
// Simple class
class Person {
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance method
greet() {
return `Hello, I'm ${this.name}`;
}
// Instance method
getAge() {
return this.age;
}
}
// Create instances
const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(alice.getAge()); // 30
console.log(bob.greet()); // "Hello, I'm Bob"
// Methods are on prototype
console.log(alice.greet === bob.greet); // true (shared method)
// instanceof check
console.log(alice instanceof Person); // true
// Class is not hoisted
// const p = new MyClass(); // ReferenceError
// class MyClass {}
// Classes are strict mode by default
class StrictClass {
constructor() {
// Implicit strict mode
// x = 10; // ReferenceError (no implicit globals)
}
}
Example: Constructor with field declarations
// Class with field declarations (ES2022)
class User {
// Public fields (initialized on instance)
id = 0;
name = 'Anonymous';
role = 'user';
createdAt = Date.now();
// Constructor overrides field defaults
constructor(id, name, role) {
this.id = id;
this.name = name;
if (role) this.role = role;
}
getInfo() {
return `${this.name} (${this.role})`;
}
}
const user1 = new User(1, 'Alice', 'admin');
console.log(user1.getInfo()); // "Alice (admin)"
console.log(user1.createdAt); // timestamp
const user2 = new User(2, 'Bob');
console.log(user2.role); // "user" (default)
// Fields are instance properties (not on prototype)
console.log(user1.hasOwnProperty('name')); // true
console.log('name' in User.prototype); // false
// Methods are on prototype
console.log(user1.hasOwnProperty('getInfo')); // false
console.log('getInfo' in User.prototype); // true
Example: Constructor patterns and validation
// Constructor with validation
class Rectangle {
constructor(width, height) {
if (width <= 0 || height <= 0) {
throw new Error('Dimensions must be positive');
}
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area()); // 50
// const invalid = new Rectangle(-5, 10); // Error
// Constructor with default parameters
class Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
distance(other) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
const p1 = new Point(3, 4);
const p2 = new Point(); // (0, 0)
console.log(p1.distance(p2)); // 5
// Constructor with destructuring
class Config {
constructor({host, port = 3000, secure = false} = {}) {
this.host = host || 'localhost';
this.port = port;
this.secure = secure;
}
getUrl() {
const protocol = this.secure ? 'https' : 'http';
return `${protocol}://${this.host}:${this.port}`;
}
}
const config1 = new Config({host: 'example.com', secure: true});
console.log(config1.getUrl()); // "https://example.com:3000"
const config2 = new Config();
console.log(config2.getUrl()); // "http://localhost:3000"
// Constructor with rest parameters
class Team {
constructor(name, ...members) {
this.name = name;
this.members = members;
}
addMember(member) {
this.members.push(member);
}
}
const team = new Team('Development', 'Alice', 'Bob', 'Charlie');
console.log(team.members); // ['Alice', 'Bob', 'Charlie']
Example: Constructor return behavior
// Normal constructor - returns instance
class Normal {
constructor(value) {
this.value = value;
}
}
const n = new Normal(5);
console.log(n instanceof Normal); // true
console.log(n.value); // 5
// Constructor returning object - overrides instance
class Custom {
constructor(value) {
this.value = value;
// Returning object overrides the instance
return {
value: value * 2,
custom: true
};
}
}
const c = new Custom(5);
console.log(c instanceof Custom); // false (returned object)
console.log(c.value); // 10
console.log(c.custom); // true
// Returning primitive - ignored
class Primitive {
constructor(value) {
this.value = value;
return 42; // Ignored
}
}
const p = new Primitive(5);
console.log(p instanceof Primitive); // true
console.log(p.value); // 5
// Singleton pattern with constructor
class Singleton {
static #instance;
constructor() {
if (Singleton.#instance) {
return Singleton.#instance;
}
this.createdAt = Date.now();
Singleton.#instance = this;
}
}
const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true (same instance)
Key Points: Classes are not hoisted (temporal dead zone).
Constructor runs when
new is called. Only one constructor per class.
Fields are instance properties; methods are prototype properties. Always in strict
mode. Can return object from constructor to override instance.
17.2 Instance Methods and Static Methods
Method Types Comparison
| Method Type | Syntax | Location | Access via | this refers to |
|---|---|---|---|---|
| Instance Method | method() {} |
Class.prototype | instance.method() | Instance object |
| Static Method | static method() {} |
Class itself | Class.method() | Class constructor |
| Private Instance | #method() {} |
Class (hidden) | this.#method() (internal) | Instance object |
| Private Static | static #method() {} |
Class (hidden) | Class.#method() (internal) | Class constructor |
Method Features
| Feature | Instance Methods | Static Methods |
|---|---|---|
| Access to this | Instance properties and methods | Static properties and methods |
| Inheritance | Inherited by subclasses | Inherited by subclasses |
| Shared | Shared among all instances | One copy on class |
| Use Case | Instance-specific behavior | Utility functions, factories, helpers |
| Call without new | ❌ Needs instance | ✓ Can call directly |
Example: Instance methods
class Calculator {
constructor(value = 0) {
this.value = value;
}
// Instance methods - operate on instance data
add(n) {
this.value += n;
return this; // Method chaining
}
subtract(n) {
this.value -= n;
return this;
}
multiply(n) {
this.value *= n;
return this;
}
divide(n) {
if (n === 0) throw new Error('Division by zero');
this.value /= n;
return this;
}
getValue() {
return this.value;
}
reset() {
this.value = 0;
return this;
}
}
const calc = new Calculator(10);
const result = calc.add(5).multiply(2).subtract(10).getValue();
console.log(result); // 20
// Methods are on prototype
console.log(calc.hasOwnProperty('add')); // false
console.log('add' in Calculator.prototype); // true
// Shared across instances
const calc2 = new Calculator();
console.log(calc.add === calc2.add); // true (same function)
Example: Static methods
class MathUtils {
// Static methods - utility functions
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
static max(...numbers) {
return Math.max(...numbers);
}
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Static method accessing other static methods
static average(...numbers) {
const sum = numbers.reduce((a, b) => this.add(a, b), 0);
return sum / numbers.length;
}
}
// Call static methods on class (not instances)
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.max(1, 5, 3, 9, 2)); // 9
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.average(1, 2, 3, 4)); // 2.5
// Static methods are on class, not prototype
console.log('add' in MathUtils); // true
console.log('add' in MathUtils.prototype); // false
// Can't call on instances
const util = new MathUtils();
// util.add(1, 2); // TypeError: util.add is not a function
// Factory pattern with static methods
class User {
constructor(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
}
// Static factory methods
static createAdmin(name, email) {
return new User(name, email, 'admin');
}
static createGuest(name) {
return new User(name, `${name}@guest.local`, 'guest');
}
static fromJSON(json) {
const data = JSON.parse(json);
return new User(data.name, data.email, data.role);
}
}
const admin = User.createAdmin('Alice', 'alice@example.com');
const guest = User.createGuest('Bob');
const parsed = User.fromJSON('{"name":"Charlie","email":"c@e.com","role":"user"}');
console.log(admin.role); // "admin"
console.log(guest.email); // "bob@guest.local"
Example: Mixing instance and static methods
class Counter {
// Static field for counting all instances
static totalInstances = 0;
static #allCounters = [];
// Instance field
value = 0;
constructor(initialValue = 0) {
this.value = initialValue;
Counter.totalInstances++;
Counter.#allCounters.push(this);
}
// Instance methods
increment() {
this.value++;
return this;
}
decrement() {
this.value--;
return this;
}
getValue() {
return this.value;
}
// Static methods
static getTotalInstances() {
return Counter.totalInstances;
}
static getSumOfAll() {
return Counter.#allCounters.reduce((sum, counter) => sum + counter.value, 0);
}
static resetAll() {
Counter.#allCounters.forEach(counter => counter.value = 0);
}
static getMaxValue() {
if (Counter.#allCounters.length === 0) return null;
return Math.max(...Counter.#allCounters.map(c => c.value));
}
}
const c1 = new Counter(5);
const c2 = new Counter(10);
const c3 = new Counter(3);
console.log(Counter.getTotalInstances()); // 3
console.log(Counter.getSumOfAll()); // 18
c1.increment().increment();
console.log(Counter.getSumOfAll()); // 20
console.log(Counter.getMaxValue()); // 10
Counter.resetAll();
console.log(c1.getValue()); // 0
console.log(c2.getValue()); // 0
Example: Static method inheritance
class Animal {
constructor(name) {
this.name = name;
}
// Instance method
speak() {
return `${this.name} makes a sound`;
}
// Static method
static info() {
return 'Animals are living organisms';
}
static compare(a, b) {
return a.name.localeCompare(b.name);
}
}
class Dog extends Animal {
// Override instance method
speak() {
return `${this.name} barks`;
}
// Static method in derived class
static info() {
return super.info() + ', and dogs are mammals';
}
}
// Static methods are inherited
console.log(Dog.info()); // "Animals are living organisms, and dogs are mammals"
const dog1 = new Dog('Max');
const dog2 = new Dog('Buddy');
// Inherited static method works with derived class instances
console.log(Dog.compare(dog1, dog2)); // Compares names
// Instance methods
console.log(dog1.speak()); // "Max barks"
Best Practices: Use instance methods for behavior specific to
each object. Use static methods for utilities, factories, and class-level
operations. Static methods can't access instance properties (no this referring to instance). Both instance and
static methods are inherited. Use static for constants, configuration, and helper functions.
17.3 Class Inheritance and super Keyword
Inheritance Syntax
| Syntax | Description | Usage |
|---|---|---|
| extends | class Child extends Parent |
Create subclass inheriting from parent |
| super() | super(args) |
Call parent constructor (required in child constructor) |
| super.method() | super.methodName(args) |
Call parent's instance method |
| super.property | super.prop |
Access parent's property or method |
| Static super | super.staticMethod() |
Call parent's static method (in static context) |
super Rules
| Rule | Description | Example/Note |
|---|---|---|
| Constructor super | Must call super() before accessing this | ReferenceError if accessing this before super() |
| No constructor | Default constructor calls super(...args) | Automatically generated if omitted |
| Return before super | Can return object without calling super() | Only if returning object explicitly |
| Method context | super only in class methods | Not in arrow functions or regular functions |
| Static super | In static method, super refers to parent class | Access static methods/properties |
Example: Basic inheritance
// Parent class
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
return `${this.name} makes a sound`;
}
getAge() {
return this.age;
}
}
// Child class
class Dog extends Animal {
constructor(name, age, breed) {
// Must call super() before accessing this
super(name, age); // Call parent constructor
this.breed = breed;
}
// Override parent method
speak() {
return `${this.name} barks`;
}
// New method specific to Dog
fetch() {
return `${this.name} fetches the ball`;
}
}
const dog = new Dog('Max', 3, 'Labrador');
console.log(dog.name); // "Max" (from parent)
console.log(dog.breed); // "Labrador" (from child)
console.log(dog.speak()); // "Max barks" (overridden)
console.log(dog.getAge()); // 3 (inherited)
console.log(dog.fetch()); // "Max fetches the ball" (child method)
// Prototype chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
// Inheritance chain
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
Example: super in methods
class Shape {
constructor(color) {
this.color = color;
}
describe() {
return `A ${this.color} shape`;
}
getColor() {
return this.color;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
// Call parent method and extend
describe() {
const base = super.describe(); // Call parent's describe()
return `${base} with radius ${this.radius}`;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle('red', 5);
console.log(circle.describe()); // "A red shape with radius 5"
console.log(circle.getColor()); // "red" (inherited)
console.log(circle.area()); // 78.54...
// More complex example
class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
describe() {
// Extend parent's description
return super.describe() + ` (${this.width}x${this.height})`;
}
area() {
return this.width * this.height;
}
// Method using both parent and child properties
getFullDescription() {
return `${super.describe()}, area: ${this.area()}`;
}
}
const rect = new Rectangle('blue', 10, 5);
console.log(rect.describe()); // "A blue shape (10x5)"
console.log(rect.getFullDescription()); // "A blue shape, area: 50"
Example: Multi-level inheritance
// Three levels of inheritance
class LivingBeing {
constructor(name) {
this.name = name;
this.alive = true;
}
breathe() {
return `${this.name} is breathing`;
}
}
class Animal2 extends LivingBeing {
constructor(name, species) {
super(name);
this.species = species;
}
move() {
return `${this.name} is moving`;
}
identify() {
return `${this.name} is a ${this.species}`;
}
}
class Dog2 extends Animal2 {
constructor(name, breed) {
super(name, 'dog'); // Species is always 'dog'
this.breed = breed;
}
bark() {
return `${this.name} barks`;
}
// Override with super call
identify() {
return super.identify() + ` (${this.breed})`;
}
}
const dog2 = new Dog2('Rex', 'German Shepherd');
console.log(dog2.breathe()); // "Rex is breathing" (from LivingBeing)
console.log(dog2.move()); // "Rex is moving" (from Animal2)
console.log(dog2.bark()); // "Rex barks" (from Dog2)
console.log(dog2.identify()); // "Rex is a dog (German Shepherd)"
// Prototype chain
console.log(dog2 instanceof Dog2); // true
console.log(dog2 instanceof Animal2); // true
console.log(dog2 instanceof LivingBeing); // true
Example: Static method inheritance
class BaseModel {
static tableName = 'base';
constructor(data) {
this.data = data;
}
// Static method
static findAll() {
console.log(`Finding all from ${this.tableName}`);
return [];
}
static findById(id) {
console.log(`Finding ${id} from ${this.tableName}`);
return null;
}
// Static method calling other static methods
static findOrCreate(id) {
let record = this.findById(id);
if (!record) {
console.log(`Creating new record in ${this.tableName}`);
record = new this({id});
}
return record;
}
}
class User2 extends BaseModel {
static tableName = 'users';
// Override static method
static findById(id) {
console.log(`[User] Finding user ${id}`);
return super.findById(id); // Can call parent's static method
}
// New static method
static findByEmail(email) {
console.log(`Finding user with email ${email} from ${this.tableName}`);
return null;
}
}
// Static methods are inherited
User2.findAll(); // "Finding all from users"
User2.findById(1); // "[User] Finding user 1" then "Finding 1 from users"
User2.findByEmail('a@b.com'); // "Finding user with email a@b.com from users"
// Static method using this correctly
const user = User2.findOrCreate(5);
console.log(user instanceof User2); // true
Important:
super() must be called before accessing
this in derived constructor. super in methods refers to parent's prototype.
super
in static methods refers to parent class. Method overriding creates new method; use super.method()
to call parent version. Prototype chain: Child → Parent → ... → Object.
17.4 Private Fields and Private Methods
Private Member Types
| Type | Syntax | Access | Inheritance |
|---|---|---|---|
| Private Field | #field = value |
Only within class body | Not inherited |
| Private Method | #method() {} |
Only within class body | Not inherited |
| Private Getter | get #prop() {} |
Only within class body | Not inherited |
| Private Setter | set #prop(v) {} |
Only within class body | Not inherited |
| Private Static Field | static #field = value |
Only within class (static context) | Not inherited |
| Private Static Method | static #method() {} |
Only within class (static context) | Not inherited |
Private vs Public Comparison
| Feature | Public | Private (#) |
|---|---|---|
| Access Outside Class | ✓ Yes | ✗ No (SyntaxError) |
| Access via this | this.field | this.#field |
| Inheritance | ✓ Inherited | ✗ Not inherited |
| Bracket Notation | ✓ obj['field'] | ✗ Not accessible |
| Reflection | ✓ Object.keys, etc. | ✗ Not enumerable |
| Name Collision | Can collide with child | Each class has separate namespace |
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.#logTransaction('Initial deposit', initialBalance);
}
// Private method
#logTransaction(type, amount) {
this.#transactions.push({
type,
amount,
date: new Date(),
balance: this.#balance
});
}
// Private validation method
#validateAmount(amount) {
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('Invalid amount');
}
}
// Public methods using private fields/methods
deposit(amount) {
this.#validateAmount(amount);
this.#balance += amount;
this.#logTransaction('Deposit', amount);
return this.#balance;
}
withdraw(amount) {
this.#validateAmount(amount);
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
this.#logTransaction('Withdrawal', -amount);
return this.#balance;
}
getBalance() {
return this.#balance;
}
getTransactionHistory() {
// Return copy to prevent external modification
return [...this.#transactions];
}
// Private getter
get #formattedBalance() {
return `${this.#balance.toFixed(2)}`;
}
toString() {
return `Account ${this.#accountNumber}: ${this.#formattedBalance}`;
}
}
const account = new BankAccount('1234567890', 1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
account.withdraw(200);
console.log(account.getBalance()); // 1300
// Private fields are truly private
console.log(account.#balance); // SyntaxError
console.log(account['#balance']); // undefined
console.log(account.balance); // undefined
// Private methods not accessible
// account.#logTransaction('test', 100); // SyntaxError
console.log(account.toString()); // "Account 1234567890: $1300.00"
Example: Private static members
class IDGenerator {
// Private static field
static #currentId = 0;
static #usedIds = new Set();
// Public instance field
id;
constructor() {
this.id = IDGenerator.#generateId();
}
// Private static method
static #generateId() {
let newId;
do {
newId = ++IDGenerator.#currentId;
} while (IDGenerator.#usedIds.has(newId));
IDGenerator.#usedIds.add(newId);
return newId;
}
// Public static method
static releaseId(id) {
IDGenerator.#usedIds.delete(id);
}
// Public static method accessing private static
static getTotalGenerated() {
return IDGenerator.#currentId;
}
static getActiveCount() {
return IDGenerator.#usedIds.size;
}
}
const obj1 = new IDGenerator();
const obj2 = new IDGenerator();
const obj3 = new IDGenerator();
console.log(obj1.id); // 1
console.log(obj2.id); // 2
console.log(obj3.id); // 3
console.log(IDGenerator.getTotalGenerated()); // 3
console.log(IDGenerator.getActiveCount()); // 3
IDGenerator.releaseId(2);
console.log(IDGenerator.getActiveCount()); // 2
// Private static members not accessible
// console.log(IDGenerator.#currentId); // SyntaxError
Example: Private fields with inheritance
class Base {
#privateBase = 'base private';
publicBase = 'base public';
getPrivateBase() {
return this.#privateBase;
}
}
class Derived extends Base {
#privateDerived = 'derived private';
publicDerived = 'derived public';
// Can have same name as parent's private field (separate namespace)
#privateBase = 'derived has its own #privateBase';
getPrivateDerived() {
return this.#privateDerived;
}
getDerivedPrivateBase() {
return this.#privateBase; // Accesses Derived's #privateBase
}
getAllInfo() {
return {
// Can access parent's private via parent's public method
parentPrivate: this.getPrivateBase(), // "base private"
// Own private fields
derivedPrivate: this.#privateDerived,
derivedPrivateBase: this.#privateBase,
// Public fields
publicBase: this.publicBase,
publicDerived: this.publicDerived
};
}
}
const derived = new Derived();
console.log(derived.getAllInfo());
// {
// parentPrivate: "base private",
// derivedPrivate: "derived private",
// derivedPrivateBase: "derived has its own #privateBase",
// publicBase: "base public",
// publicDerived: "derived public"
// }
// Each class has separate private namespace
console.log(derived.getPrivateBase()); // "base private"
console.log(derived.getDerivedPrivateBase()); // "derived has its own #privateBase"
Example: Practical private member use case
class SecureCache {
// Private storage
#cache = new Map();
#accessLog = [];
#maxSize = 100;
// Private method for logging
#log(operation, key) {
this.#accessLog.push({
operation,
key,
timestamp: Date.now()
});
// Keep log size manageable
if (this.#accessLog.length > 1000) {
this.#accessLog = this.#accessLog.slice(-500);
}
}
// Private method for size check
#checkSize() {
if (this.#cache.size >= this.#maxSize) {
// Remove oldest entry (first key)
const firstKey = this.#cache.keys().next().value;
this.#cache.delete(firstKey);
}
}
// Public API
set(key, value) {
this.#checkSize();
this.#cache.set(key, value);
this.#log('set', key);
}
get(key) {
this.#log('get', key);
return this.#cache.get(key);
}
has(key) {
return this.#cache.has(key);
}
delete(key) {
this.#log('delete', key);
return this.#cache.delete(key);
}
clear() {
this.#cache.clear();
this.#log('clear', null);
}
// Private getter
get #stats() {
return {
size: this.#cache.size,
maxSize: this.#maxSize,
accessCount: this.#accessLog.length
};
}
getStats() {
return {...this.#stats}; // Return copy
}
getRecentAccess(count = 10) {
return this.#accessLog.slice(-count);
}
}
const cache = new SecureCache();
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.get('key1');
console.log(cache.getStats());
// {size: 2, maxSize: 100, accessCount: 3}
console.log(cache.getRecentAccess(5));
// Recent access log
// Internal state completely private
// cache.#cache // SyntaxError
// cache.#log() // SyntaxError
Key Points: Private members use
# prefix and must be declared in class body. Truly private (not accessible outside class body). Not
inherited by subclasses. Each class has separate private namespace (can have same names). Use for encapsulation, internal implementation details, sensitive data. Check existence with
#field in obj (inside class only).
17.5 Getters and Setters in Classes
Accessor Types
| Accessor | Syntax | Purpose | Usage |
|---|---|---|---|
| Getter | get propName() {} |
Read computed property | obj.propName |
| Setter | set propName(value) {} |
Write with validation | obj.propName = value |
| Private Getter | get #propName() {} |
Private computed property | this.#propName |
| Private Setter | set #propName(v) {} |
Private validated write | this.#propName = v |
| Static Getter | static get propName() {} |
Class-level computed property | Class.propName |
| Static Setter | static set propName(v) {} |
Class-level validated write | Class.propName = v |
Getter/Setter Features
| Feature | Description | Behavior |
|---|---|---|
| Access Syntax | Use like property (no parentheses) | obj.prop not obj.prop() |
| Computed Values | Calculate value on access | No storage, derived from other properties |
| Validation | Validate before setting | Throw error or coerce value |
| Lazy Evaluation | Compute only when accessed | Can cache result |
| Side Effects | Can trigger actions on get/set | Logging, updates, notifications |
| Read-only | Getter without setter | Assignment ignored in non-strict, error in strict |
| Write-only | Setter without getter | Reading returns undefined |
Example: Basic getters and setters
class Person {
constructor(firstName, lastName, birthYear) {
this.firstName = firstName;
this.lastName = lastName;
this.birthYear = birthYear;
}
// Getter - computed property
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// Setter - split and assign
set fullName(name) {
const [first, last] = name.split(' ');
this.firstName = first;
this.lastName = last;
}
// Getter - computed from other property
get age() {
return new Date().getFullYear() - this.birthYear;
}
// Read-only (getter without setter)
get initials() {
return `${this.firstName[0]}.${this.lastName[0]}.`;
}
}
const person = new Person('John', 'Doe', 1990);
// Access like properties (no parentheses)
console.log(person.fullName); // "John Doe"
console.log(person.age); // 35 (computed)
console.log(person.initials); // "J.D."
// Use setter
person.fullName = 'Jane Smith';
console.log(person.firstName); // "Jane"
console.log(person.lastName); // "Smith"
console.log(person.fullName); // "Jane Smith"
// Read-only property
person.initials = 'X.Y.'; // Ignored (non-strict) or error (strict)
console.log(person.initials); // Still "J.S."
// Age is computed each time
console.log(person.age); // Computed from current year
Example: Validation with setters
class Product {
#price = 0;
#quantity = 0;
constructor(name) {
this.name = name;
}
// Getter returns private field
get price() {
return this.#price;
}
// Setter with validation
set price(value) {
if (typeof value !== 'number') {
throw new TypeError('Price must be a number');
}
if (value < 0) {
throw new RangeError('Price cannot be negative');
}
this.#price = value;
}
get quantity() {
return this.#quantity;
}
set quantity(value) {
if (!Number.isInteger(value)) {
throw new TypeError('Quantity must be an integer');
}
if (value < 0) {
throw new RangeError('Quantity cannot be negative');
}
this.#quantity = value;
}
// Computed property
get total() {
return this.#price * this.#quantity;
}
// Computed with formatting
get formattedTotal() {
return `${this.total.toFixed(2)}`;
}
}
const product = new Product('Widget');
product.price = 19.99;
product.quantity = 5;
console.log(product.price); // 19.99
console.log(product.quantity); // 5
console.log(product.total); // 99.95
console.log(product.formattedTotal); // "$99.95"
// Validation in action
try {
product.price = -10; // RangeError: Price cannot be negative
} catch (e) {
console.error(e.message);
}
try {
product.quantity = 3.5; // TypeError: Quantity must be an integer
} catch (e) {
console.error(e.message);
}
Example: Lazy evaluation and caching
class DataProcessor {
#data;
#processedCache = null;
#isDirty = true;
constructor(data) {
this.#data = data;
}
// Setter invalidates cache
set data(newData) {
this.#data = newData;
this.#isDirty = true; // Mark cache as invalid
this.#processedCache = null;
}
get data() {
return this.#data;
}
// Lazy evaluation with caching
get processed() {
if (this.#isDirty || this.#processedCache === null) {
console.log('Computing processed data...');
// Expensive operation
this.#processedCache = this.#data.map(x => x * 2);
this.#isDirty = false;
} else {
console.log('Using cached data');
}
return this.#processedCache;
}
// Read-only computed statistics
get stats() {
const data = this.processed;
return {
count: data.length,
sum: data.reduce((a, b) => a + b, 0),
average: data.reduce((a, b) => a + b, 0) / data.length,
min: Math.min(...data),
max: Math.max(...data)
};
}
}
const processor = new DataProcessor([1, 2, 3, 4, 5]);
console.log(processor.processed); // Logs: "Computing...", returns [2, 4, 6, 8, 10]
console.log(processor.processed); // Logs: "Using cached data", returns same
console.log(processor.stats); // Computes statistics
processor.data = [10, 20, 30]; // Invalidates cache
console.log(processor.processed); // Logs: "Computing...", returns [20, 40, 60]
Example: Static getters and setters
class Configuration {
static #config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
};
// Static getter
static get apiUrl() {
return Configuration.#config.apiUrl;
}
// Static setter with validation
static set apiUrl(url) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new Error('URL must start with http:// or https://');
}
Configuration.#config.apiUrl = url;
}
static get timeout() {
return Configuration.#config.timeout;
}
static set timeout(ms) {
if (ms < 0) throw new Error('Timeout cannot be negative');
Configuration.#config.timeout = ms;
}
static get debug() {
return Configuration.#config.debug;
}
static set debug(value) {
Configuration.#config.debug = Boolean(value);
}
// Read-only computed configuration
static get summary() {
return `API: ${Configuration.apiUrl}, Timeout: ${Configuration.timeout}ms`;
}
// Method to get all config (snapshot)
static getAll() {
return {...Configuration.#config};
}
}
// Access static getters/setters on class
console.log(Configuration.apiUrl); // "https://api.example.com"
console.log(Configuration.timeout); // 5000
Configuration.apiUrl = 'https://new-api.com';
Configuration.timeout = 10000;
Configuration.debug = true;
console.log(Configuration.summary);
// "API: https://new-api.com, Timeout: 10000ms"
console.log(Configuration.getAll());
// {apiUrl: "https://new-api.com", timeout: 10000, debug: true}
Example: Private getters and setters
class Temperature {
#celsius = 0;
constructor(celsius) {
this.celsius = celsius; // Use public setter
}
// Public getter/setter for Celsius
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (value < -273.15) {
throw new Error('Below absolute zero');
}
this.#celsius = value;
}
// Public getter/setter for Fahrenheit
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // Use celsius setter
}
// Private getter - internal formatting
get #formatted() {
return {
c: `${this.#celsius.toFixed(1)}°C`,
f: `${this.fahrenheit.toFixed(1)}°F`
};
}
// Public method using private getter
toString() {
const fmt = this.#formatted;
return `${fmt.c} (${fmt.f})`;
}
// Computed properties
get kelvin() {
return this.#celsius + 273.15;
}
get description() {
if (this.#celsius < 0) return 'Freezing';
if (this.#celsius < 10) return 'Cold';
if (this.#celsius < 20) return 'Cool';
if (this.#celsius < 30) return 'Warm';
return 'Hot';
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
console.log(temp.description); // "Warm"
console.log(temp.toString()); // "25.0°C (77.0°F)"
temp.fahrenheit = 32; // Set via Fahrenheit
console.log(temp.celsius); // 0
console.log(temp.description); // "Freezing"
Best Practices: Use getters for computed properties and
data transformation. Use setters for validation
and side effects. Getters should be fast (cache if expensive). Avoid side
effects in getters when possible. Combine with private fields for true encapsulation. Setters can coerce values
or throw errors.
17.6 Class Expressions and Anonymous Classes
Class Expression Types
| Type | Syntax | Name | Use Case |
|---|---|---|---|
| Named Class Expression | const C = class Name {} |
Name only inside class | Recursion, debugging |
| Anonymous Class Expression | const C = class {} |
No internal name | Simple assignments |
| IIFE Class | (class {})() |
Anonymous | Immediate instantiation |
| Inline Class | func(class {}) |
Anonymous | Factory functions, callbacks |
Class Expression vs Declaration
| Feature | Class Declaration | Class Expression |
|---|---|---|
| Hoisting | Not hoisted (TDZ) | Not hoisted (like var/let/const) |
| Name | Required | Optional |
| Assignment | Creates binding | Can assign to variable |
| Inline Usage | ❌ Cannot use inline | ✓ Can use as expression |
| Conditional | ❌ Cannot be conditional | ✓ Can be conditional |
Example: Class expressions
// Anonymous class expression
const Person1 = class {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
};
const p1 = new Person1('Alice');
console.log(p1.greet()); // "Hello, I'm Alice"
console.log(Person1.name); // "" (anonymous)
// Named class expression
const Person2 = class PersonClass {
constructor(name) {
this.name = name;
}
greet() {
// Name available inside class
return `Hello from ${PersonClass.name}`;
}
clone() {
// Can use internal name for recursion
return new PersonClass(this.name);
}
};
const p2 = new Person2('Bob');
console.log(p2.greet()); // "Hello from PersonClass"
console.log(Person2.name); // "PersonClass"
// Internal name not accessible outside
// console.log(PersonClass); // ReferenceError
// Reassignment
const OldPerson = Person2;
const Person3 = class extends OldPerson {
greet() {
return super.greet() + ' (extended)';
}
};
const p3 = new Person3('Charlie');
console.log(p3.greet()); // "Hello from PersonClass (extended)"
Example: Conditional class creation
// Create different classes based on condition
function createUserClass(type) {
if (type === 'admin') {
return class AdminUser {
constructor(name) {
this.name = name;
this.role = 'admin';
}
manage() {
return `${this.name} is managing`;
}
};
} else {
return class RegularUser {
constructor(name) {
this.name = name;
this.role = 'user';
}
view() {
return `${this.name} is viewing`;
}
};
}
}
const AdminClass = createUserClass('admin');
const UserClass = createUserClass('user');
const admin = new AdminClass('Alice');
const user = new UserClass('Bob');
console.log(admin.manage()); // "Alice is managing"
console.log(user.view()); // "Bob is viewing"
// Environment-specific classes
const Storage = typeof window !== 'undefined'
? class BrowserStorage {
save(key, value) {
localStorage.setItem(key, value);
}
load(key) {
return localStorage.getItem(key);
}
}
: class NodeStorage {
#data = new Map();
save(key, value) {
this.#data.set(key, value);
}
load(key) {
return this.#data.get(key);
}
};
const storage = new Storage();
storage.save('key', 'value');
Example: Inline and IIFE classes
// Inline class as argument
function createInstance(ClassDef, ...args) {
return new ClassDef(...args);
}
const point = createInstance(class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}, 3, 4);
console.log(point.distance()); // 5
// IIFE class - immediate instantiation
const singleton = new class Singleton {
constructor() {
this.id = Math.random();
this.createdAt = Date.now();
}
getId() {
return this.id;
}
}();
console.log(singleton.getId()); // Random number
// Array of different classes
const shapes = [
new (class Circle {
constructor(r) { this.radius = r; }
area() { return Math.PI * this.radius ** 2; }
})(5),
new (class Rectangle {
constructor(w, h) { this.width = w; this.height = h; }
area() { return this.width * this.height; }
})(4, 6),
new (class Triangle {
constructor(b, h) { this.base = b; this.height = h; }
area() { return 0.5 * this.base * this.height; }
})(3, 4)
];
shapes.forEach(shape => console.log(shape.area()));
// 78.54, 24, 6
// Factory with inline class
function createPlugin(name, handler) {
return new (class Plugin {
constructor() {
this.name = name;
this.handler = handler;
}
execute(...args) {
return this.handler(...args);
}
})();
}
const logger = createPlugin('logger', (msg) => console.log(`[LOG] ${msg}`));
logger.execute('Hello'); // "[LOG] Hello"
Example: Mixin pattern with class expressions
// Class expression for mixins
const Flyable = Base => class extends Base {
fly() {
return `${this.name} is flying`;
}
land() {
return `${this.name} has landed`;
}
};
const Swimmable = Base => class extends Base {
swim() {
return `${this.name} is swimming`;
}
};
const Runnable = Base => class extends Base {
run() {
return `${this.name} is running`;
}
};
// Base class
class Animal {
constructor(name) {
this.name = name;
}
}
// Compose abilities
class Duck extends Swimmable(Flyable(Animal)) {
quack() {
return `${this.name} says quack`;
}
}
class Fish extends Swimmable(Animal) {
// Fish can only swim
}
class Bird extends Flyable(Runnable(Animal)) {
// Bird can fly and run
}
const duck = new Duck('Donald');
console.log(duck.fly()); // "Donald is flying"
console.log(duck.swim()); // "Donald is swimming"
console.log(duck.quack()); // "Donald says quack"
const fish = new Fish('Nemo');
console.log(fish.swim()); // "Nemo is swimming"
// fish.fly(); // TypeError
const bird = new Bird('Tweety');
console.log(bird.fly()); // "Tweety is flying"
console.log(bird.run()); // "Tweety is running"
Use Cases: Class expressions enable conditional class creation,
factory patterns, mixins, and inline class definitions. Named expressions provide internal name for recursion and
debugging. Anonymous expressions are simpler for one-time use. IIFE classes for immediate instantiation.
17.7 Mixin Patterns and Class Composition
Mixin Approaches
| Approach | Method | Pros | Cons |
|---|---|---|---|
| Functional Mixin | Function that modifies prototype | Simple, flexible | Pollutes prototype |
| Subclass Factory | Function returning class extending Base | Proper inheritance, composable | More complex |
| Object.assign | Copy methods to prototype | Very simple | Shallow, no super |
| Symbol-based | Use symbols to avoid collisions | No name collisions | Less discoverable |
Composition Patterns
| Pattern | Description | Use When |
|---|---|---|
| Multiple Inheritance | Combine multiple behaviors | Need features from multiple sources |
| Trait-based | Small reusable units of behavior | Fine-grained feature composition |
| Delegation | Forward calls to composed objects | Prefer composition over inheritance |
| Strategy | Swap implementations at runtime | Need dynamic behavior changes |
Example: Subclass factory mixins
// Mixin functions returning class
const TimestampMixin = Base => class extends Base {
constructor(...args) {
super(...args);
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
touch() {
this.updatedAt = Date.now();
}
getAge() {
return Date.now() - this.createdAt;
}
};
const ValidationMixin = Base => class extends Base {
validate() {
// Validate implementation
return this.isValid();
}
isValid() {
// Override in subclass
return true;
}
};
const SerializableMixin = Base => class extends Base {
toJSON() {
// Get all enumerable properties
return Object.keys(this).reduce((obj, key) => {
obj[key] = this[key];
return obj;
}, {});
}
static fromJSON(json) {
const data = typeof json === 'string' ? JSON.parse(json) : json;
return new this(data);
}
};
// Base class
class Entity {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
// Compose mixins
class User extends SerializableMixin(ValidationMixin(TimestampMixin(Entity))) {
constructor(id, name, email) {
super(id, name);
this.email = email;
}
// Override validation
isValid() {
return this.email && this.email.includes('@');
}
}
const user = new User(1, 'Alice', 'alice@example.com');
console.log(user.validate()); // true
console.log(user.getAge()); // Time since creation
user.touch();
console.log(user.updatedAt > user.createdAt); // true
const json = user.toJSON();
console.log(json); // {id: 1, name: "Alice", email: "alice@example.com", ...}
const restored = User.fromJSON(json);
console.log(restored.name); // "Alice"
Example: Object.assign mixin pattern
// Simple mixin objects
const EventEmitterMixin = {
on(event, handler) {
this._handlers = this._handlers || {};
this._handlers[event] = this._handlers[event] || [];
this._handlers[event].push(handler);
return this;
},
off(event, handler) {
if (!this._handlers || !this._handlers[event]) return;
if (handler) {
const index = this._handlers[event].indexOf(handler);
if (index > -1) {
this._handlers[event].splice(index, 1);
}
} else {
delete this._handlers[event];
}
return this;
},
emit(event, ...args) {
if (!this._handlers || !this._handlers[event]) return;
this._handlers[event].forEach(handler => {
handler.apply(this, args);
});
return this;
}
};
const LoggableMixin = {
log(message) {
console.log(`[${this.constructor.name}] ${message}`);
},
error(message) {
console.error(`[${this.constructor.name}] ERROR: ${message}`);
}
};
// Apply mixins to class
class Component {
constructor(name) {
this.name = name;
}
}
// Mix in methods
Object.assign(Component.prototype, EventEmitterMixin, LoggableMixin);
const comp = new Component('MyComponent');
// Use mixed-in methods
comp.log('Component created'); // "[Component] Component created"
comp.on('update', (data) => {
console.log('Updated:', data);
});
comp.emit('update', {value: 42}); // "Updated: {value: 42}"
// Helper function for multiple mixins
function applyMixins(targetClass, ...mixins) {
mixins.forEach(mixin => {
Object.assign(targetClass.prototype, mixin);
});
}
class App {
constructor(name) {
this.name = name;
}
}
applyMixins(App, EventEmitterMixin, LoggableMixin);
const app = new App('MyApp');
app.log('App started');
app.on('error', (err) => app.error(err.message));
Example: Functional composition pattern
// Functional mixin - modifies instance
function withId(obj) {
obj.id = Math.random().toString(36).substr(2, 9);
obj.getId = function() { return this.id; };
return obj;
}
function withTimestamp(obj) {
obj.timestamp = Date.now();
obj.getTimestamp = function() { return this.timestamp; };
return obj;
}
function withVersion(obj) {
obj.version = '1.0.0';
obj.getVersion = function() { return this.version; };
return obj;
}
// Composition function
function compose(...funcs) {
return (obj) => funcs.reduce((acc, func) => func(acc), obj);
}
const enhance = compose(withId, withTimestamp, withVersion);
// Use with any object
const data = {name: 'Test'};
const enhanced = enhance(data);
console.log(enhanced.getId()); // Random ID
console.log(enhanced.getTimestamp()); // Timestamp
console.log(enhanced.getVersion()); // "1.0.0"
// Pipeline pattern
const pipeline = (...fns) => x => fns.reduce((v, f) => f(v), x);
const createUser = pipeline(
(name) => ({name}),
withId,
withTimestamp,
(obj) => {
obj.greet = function() { return `Hello, ${this.name}`; };
return obj;
}
);
const user2 = createUser('Bob');
console.log(user2.greet()); // "Hello, Bob"
console.log(user2.id); // Random ID
Example: Advanced mixin composition
// Mixin with dependencies
const RequiresName = Base => class extends Base {
constructor(...args) {
super(...args);
if (!this.name) {
throw new Error('Name is required');
}
}
};
const Describable = Base => class extends Base {
describe() {
return `${this.constructor.name}: ${this.name}`;
}
};
const Comparable = Base => class extends Base {
compareTo(other) {
if (this.name < other.name) return -1;
if (this.name > other.name) return 1;
return 0;
}
equals(other) {
return this.compareTo(other) === 0;
}
};
// Composable mixin builder
function mix(baseClass) {
return {
with(...mixins) {
return mixins.reduce((c, mixin) => mixin(c), baseClass);
}
};
}
// Build class with mixins
class Item {
constructor(name, value) {
this.name = name;
this.value = value;
}
}
const EnhancedItem = mix(Item)
.with(RequiresName, Describable, Comparable);
const item1 = new EnhancedItem('Apple', 10);
const item2 = new EnhancedItem('Banana', 5);
console.log(item1.describe()); // "EnhancedItem: Apple"
console.log(item1.compareTo(item2)); // -1 (Apple < Banana)
// Conditional mixins
function withFeature(feature, Mixin) {
return Base => {
if (feature.enabled) {
return Mixin(Base);
}
return Base;
};
}
const DebugMixin = Base => class extends Base {
debug(msg) {
console.log(`[DEBUG] ${msg}`);
}
};
const config = {debug: {enabled: true}};
const DebugItem = mix(Item)
.with(withFeature(config.debug, DebugMixin));
const debugItem = new DebugItem('Test', 1);
if (debugItem.debug) {
debugItem.debug('Item created'); // "[DEBUG] Item created"
}
Example: Trait-based composition
// Trait objects (small, focused behaviors)
const Clickable = {
click() {
this.emit('click', this);
},
onClick(handler) {
this.on('click', handler);
return this;
}
};
const Hoverable = {
hover() {
this.emit('hover', this);
},
onHover(handler) {
this.on('hover', handler);
return this;
}
};
const Draggable = {
startDrag(x, y) {
this.dragStart = {x, y};
this.emit('dragstart', this);
},
drag(x, y) {
if (!this.dragStart) return;
this.emit('drag', {x, y, start: this.dragStart});
},
endDrag() {
this.dragStart = null;
this.emit('dragend', this);
}
};
// Trait composer
function withTraits(target, ...traits) {
traits.forEach(trait => {
Object.keys(trait).forEach(key => {
if (key in target.prototype) {
console.warn(`Overriding ${key} in ${target.name}`);
}
target.prototype[key] = trait[key];
});
});
return target;
}
// Base with event system
class UIElement {
constructor(type) {
this.type = type;
this._handlers = {};
}
on(event, handler) {
this._handlers[event] = this._handlers[event] || [];
this._handlers[event].push(handler);
return this;
}
emit(event, data) {
if (this._handlers[event]) {
this._handlers[event].forEach(h => h(data));
}
return this;
}
}
// Compose specific elements
const Button = withTraits(
class Button extends UIElement {
constructor(label) {
super('button');
this.label = label;
}
},
Clickable,
Hoverable
);
const Panel = withTraits(
class Panel extends UIElement {
constructor(title) {
super('panel');
this.title = title;
}
},
Clickable,
Hoverable,
Draggable
);
const btn = new Button('Click Me');
btn.onClick(() => console.log('Button clicked'));
btn.click(); // "Button clicked"
const panel = new Panel('My Panel');
panel.onClick(() => console.log('Panel clicked'));
panel.onHover(() => console.log('Panel hovered'));
panel.startDrag(0, 0);
panel.drag(10, 10);
panel.endDrag();
Best Practices: Prefer composition over inheritance for
flexibility. Use subclass factory mixins for proper inheritance chain. Keep
mixins small and focused (single responsibility). Document mixin dependencies.
Avoid naming conflicts (use symbols if needed). Consider trait-based composition
for fine-grained features. Use composition for "has-a" relationships, inheritance for "is-a".
Section 17 Summary
- Classes: Syntactic sugar over prototypes; not hoisted; always strict mode; constructor initializes; methods on prototype; fields on instance
- Methods: Instance methods (prototype, access this), static methods (class itself, utilities/factories), inherited by subclasses
- Inheritance:
extendscreates subclass;super()calls parent constructor (required before this);super.method()calls parent methods - Private Members:
#fieldand#methodtruly private; not inherited; separate namespace per class; use for encapsulation - Getters/Setters:
get/setfor computed properties, validation, side effects; access like properties; can be private/static - Class Expressions: Named or anonymous; enable conditional creation, factories, inline usage; IIFE classes for immediate instantiation
- Mixins: Subclass factory pattern for composition;
Object.assignfor simple cases; functional composition; trait-based for fine-grained features - Composition: Prefer composition over inheritance for flexibility; combine multiple behaviors; avoid deep inheritance hierarchies; mix(...).with(mixins) pattern
18. Iterators, Generators, and Iteration Protocols
18.1 Iterator Interface and Iterable Protocol
Iterator Protocol
| Requirement | Description | Returns |
|---|---|---|
| next() method | Returns object with value and done properties | {value: any, done: boolean} |
| value | Current iteration value | Any type |
| done | true if iterator finished, false otherwise | Boolean |
Iterable Protocol
| Requirement | Description | Symbol |
|---|---|---|
| [Symbol.iterator] method | Returns iterator object with next() method | Symbol.iterator |
| Return value | Object implementing iterator protocol | Iterator with next() |
Built-in Iterables
| Type | Iterable | Iteration Order |
|---|---|---|
| Array | ✓ Yes | Index order (0, 1, 2, ...) |
| String | ✓ Yes | Character order |
| Map | ✓ Yes | Insertion order [key, value] pairs |
| Set | ✓ Yes | Insertion order values |
| TypedArray | ✓ Yes | Index order |
| arguments | ✓ Yes | Index order |
| NodeList | ✓ Yes | Index order |
| Object | ✗ No (use Object.keys/values/entries) | N/A |
Consuming Iterables
| Construct | Syntax | Description |
|---|---|---|
| for...of loop | for (const x of iterable) |
Iterate over iterable values |
| Spread operator | [...iterable] |
Convert iterable to array |
| Array.from() | Array.from(iterable) |
Create array from iterable |
| Destructuring | const [a, b] = iterable |
Extract values from iterable |
| yield* | yield* iterable |
Delegate to iterable in generator |
| Promise.all() | Promise.all(iterable) |
Wait for all promises in iterable |
| Map/Set constructor | new Map(iterable) |
Create Map/Set from iterable |
Example: Understanding iterator protocol
// Array is iterable
const arr = [1, 2, 3];
// Get iterator from iterable
const iterator = arr[Symbol.iterator]();
// Call next() manually
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
console.log(iterator.next()); // {value: undefined, done: true}
// for...of uses iterator protocol internally
for (const value of arr) {
console.log(value); // 1, 2, 3
}
// String is iterable
const str = 'hello';
const strIterator = str[Symbol.iterator]();
console.log(strIterator.next()); // {value: 'h', done: false}
console.log(strIterator.next()); // {value: 'e', done: false}
for (const char of str) {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
// Map is iterable (yields [key, value] pairs)
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // 'a' 1, 'b' 2
}
// Set is iterable
const set = new Set([1, 2, 3]);
for (const value of set) {
console.log(value); // 1, 2, 3
}
Example: Creating simple iterator
// Simple iterator (not iterable)
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
const rangeIter = createRangeIterator(1, 5);
console.log(rangeIter.next()); // {value: 1, done: false}
console.log(rangeIter.next()); // {value: 2, done: false}
console.log(rangeIter.next()); // {value: 3, done: false}
console.log(rangeIter.next()); // {value: 4, done: false}
console.log(rangeIter.next()); // {value: 5, done: false}
console.log(rangeIter.next()); // {value: undefined, done: true}
// Cannot use with for...of (not iterable, just iterator)
// for (const n of rangeIter) {} // TypeError
Example: Creating iterable object
// Iterable object (has Symbol.iterator method)
const range = {
start: 1,
end: 5,
// Implement iterable protocol
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
// Return iterator
return {
next() {
if (current <= end) {
return {
value: current++,
done: false
};
}
return {
value: undefined,
done: true
};
}
};
}
};
// Can use with for...of
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
// Can use spread operator
const arr2 = [...range];
console.log(arr2); // [1, 2, 3, 4, 5]
// Can destructure
const [first, second, ...rest] = range;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Each call to Symbol.iterator creates new iterator
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();
console.log(iter1.next()); // {value: 1, done: false}
console.log(iter2.next()); // {value: 1, done: false} (independent)
Example: Iterable with state
// Iterable collection
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
append(value) {
const node = {value, next: null};
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.length++;
}
// Make iterable
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);
// Use with for...of
for (const value of list) {
console.log(value); // 10, 20, 30
}
// Convert to array
const listArray = [...list];
console.log(listArray); // [10, 20, 30]
// Use Array methods via spread
const doubled = [...list].map(x => x * 2);
console.log(doubled); // [20, 40, 60]
Key Points: Iterator protocol: object with next() returning
{value, done}. Iterable protocol: object with [Symbol.iterator] method returning
iterator. Built-in iterables: Array, String, Map, Set, TypedArray. Iterables work with for...of, spread,
destructuring. Each call to Symbol.iterator creates fresh iterator.
18.2 Generator Functions and yield Expressions
Generator Function Syntax
| Type | Syntax | Example |
|---|---|---|
| Function Declaration | function* name() {} |
function* gen() { yield 1; } |
| Function Expression | const f = function*() {} |
const g = function*() { yield 1; } |
| Method | *method() {} |
obj = {*gen() { yield 1; }} |
| Class Method | *method() {} |
class C {*gen() { yield 1; }} |
| Arrow Function | ❌ Not supported | Cannot create generator arrows |
yield Expression Types
| Expression | Syntax | Description | Returns |
|---|---|---|---|
| yield | yield value |
Pause and yield value | Value from next() or undefined |
| yield* | yield* iterable |
Delegate to iterable/generator | Final return value of iterable |
| yield (no value) | yield |
Pause, yield undefined | Value from next() |
| yield (assignment) | const x = yield |
Receive value from next(value) | Value passed to next() |
Generator Object Properties
| Feature | Description | Behavior |
|---|---|---|
| Is Iterable | Has [Symbol.iterator]() method | Returns itself |
| Is Iterator | Has next() method | Returns {value, done} |
| Lazy | Executes on demand | Code runs only when next() called |
| Pausable | Execution pauses at yield | Resumes on next next() call |
| State | Maintains local state between yields | Variables persist across pauses |
Example: Basic generator functions
// Simple generator
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
// Generator is both iterator and iterable
console.log(typeof gen.next); // "function" (iterator)
console.log(typeof gen[Symbol.iterator]); // "function" (iterable)
// Use as iterator
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}
// Use as iterable
function* gen2() {
yield 1;
yield 2;
yield 3;
}
for (const value of gen2()) {
console.log(value); // 1, 2, 3
}
// Spread operator
const values = [...gen2()];
console.log(values); // [1, 2, 3]
// Generator with logic
function* fibonacci(n) {
let [a, b] = [0, 1];
let count = 0;
while (count < n) {
yield a;
[a, b] = [b, a + b];
count++;
}
}
console.log([...fibonacci(10)]);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Example: yield expressions and two-way communication
// Generator receiving values
function* twoWay() {
console.log('Start');
const a = yield 'First';
console.log('Received:', a);
const b = yield 'Second';
console.log('Received:', b);
return 'Done';
}
const gen3 = twoWay();
// First next() starts generator
console.log(gen3.next()); // Logs: "Start"
// Returns: {value: 'First', done: false}
// Send value 10
console.log(gen3.next(10)); // Logs: "Received: 10"
// Returns: {value: 'Second', done: false}
// Send value 20
console.log(gen3.next(20)); // Logs: "Received: 20"
// Returns: {value: 'Done', done: true}
// Practical example: ID generator
function* idGenerator(prefix = 'ID') {
let id = 1;
while (true) {
const reset = yield `${prefix}-${id}`;
if (reset) {
id = 1;
} else {
id++;
}
}
}
const ids = idGenerator('USER');
console.log(ids.next().value); // "USER-1"
console.log(ids.next().value); // "USER-2"
console.log(ids.next().value); // "USER-3"
console.log(ids.next(true).value); // "USER-1" (reset)
console.log(ids.next().value); // "USER-2"
Example: yield* delegation
// Delegate to another generator
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield 'a';
yield* gen1(); // Delegate to gen1
yield 'b';
}
console.log([...gen2()]); // ['a', 1, 2, 'b']
// Delegate to any iterable
function* gen3() {
yield* [1, 2, 3];
yield* 'hello';
yield* new Set([4, 5, 6]);
}
console.log([...gen3()]);
// [1, 2, 3, 'h', 'e', 'l', 'l', 'o', 4, 5, 6]
// Tree traversal
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); // Recursive delegation
}
}
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]
// yield* returns the final value
function* innerGen() {
yield 1;
yield 2;
return 'finished';
}
function* outerGen() {
const result = yield* innerGen();
console.log('Inner returned:', result); // "finished"
yield 3;
}
console.log([...outerGen()]);
// Logs: "Inner returned: finished"
// Returns: [1, 2, 3] (return value not included in iteration)
Example: Infinite sequences and lazy evaluation
// Infinite sequence
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
const nums = naturals();
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
console.log(nums.next().value); // 3
// Take first N
function* take(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
console.log([...take(5, naturals())]); // [1, 2, 3, 4, 5]
// Filter
function* filter(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
function* evens() {
yield* filter(n => n % 2 === 0, naturals());
}
console.log([...take(5, evens())]); // [2, 4, 6, 8, 10]
// Map
function* map(fn, iterable) {
for (const value of iterable) {
yield fn(value);
}
}
function* squares() {
yield* map(n => n ** 2, naturals());
}
console.log([...take(5, squares())]); // [1, 4, 9, 16, 25]
// Compose transformations (lazy!)
const result = take(5,
map(n => n ** 2,
filter(n => n % 2 === 0, naturals())
)
);
console.log([...result]); // [4, 16, 36, 64, 100]
// Random number generator
function* randomInRange(min, max) {
while (true) {
yield Math.floor(Math.random() * (max - min + 1)) + min;
}
}
const dice = randomInRange(1, 6);
console.log([...take(10, dice)]); // 10 random numbers 1-6
Key Points: Generators are both iterator and iterable. Execution
pauses at yield, resumes at next next().
yield* delegates to another iterable. Generators enable
lazy evaluation (compute on demand). Two-way communication: yield sends value
out,
next(value) sends value in. Perfect for infinite sequences and streaming data.
18.3 Generator Methods and Return Values
Generator Methods
| Method | Syntax | Description | Effect |
|---|---|---|---|
| next() | gen.next(value) |
Resume execution, send value to yield | Returns {value, done} |
| return() | gen.return(value) |
Terminate generator, set return value | Returns {value, done: true} |
| throw() | gen.throw(error) |
Throw error at yield point | Returns {value, done} or throws |
Generator Return Behavior
| Scenario | Result | done | value |
|---|---|---|---|
| yield expression | Normal iteration | false | Yielded value |
| return statement | Generator completes | true | Returned value |
| Implicit return (end) | Generator completes | true | undefined |
| gen.return(value) | Forced completion | true | Provided value |
| gen.throw(error) | Error thrown at yield | Depends on handling | Next yield or throws |
finally Block Behavior
| Method | finally Executes | Notes |
|---|---|---|
| return() | ✓ Yes | Cleanup code runs before completion |
| throw() | ✓ Yes | Runs before exception propagates |
| Normal completion | ✓ Yes | Runs when generator ends normally |
Example: Generator return() method
// Early termination with return()
function* gen4() {
try {
yield 1;
yield 2;
yield 3;
yield 4;
} finally {
console.log('Cleanup');
}
}
const g4 = gen4();
console.log(g4.next()); // {value: 1, done: false}
console.log(g4.next()); // {value: 2, done: false}
console.log(g4.return(99)); // Logs: "Cleanup"
// Returns: {value: 99, done: true}
console.log(g4.next()); // {value: undefined, done: true}
// return() without value
function* gen5() {
yield 1;
yield 2;
yield 3;
}
const g5 = gen5();
console.log(g5.next()); // {value: 1, done: false}
console.log(g5.return()); // {value: undefined, done: true}
console.log(g5.next()); // {value: undefined, done: true}
// Using return in for...of
function* gen6() {
yield 1;
yield 2;
return 3; // Return value not included in iteration
yield 4; // Never reached
}
const values = [...gen6()];
console.log(values); // [1, 2] (return value excluded)
// But visible with manual next()
const g6 = gen6();
console.log(g6.next()); // {value: 1, done: false}
console.log(g6.next()); // {value: 2, done: false}
console.log(g6.next()); // {value: 3, done: true} (return value visible)
Example: Generator throw() method
// Error handling with throw()
function* gen7() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e.message);
yield 'error handled';
}
yield 4;
}
const g7 = gen7();
console.log(g7.next()); // {value: 1, done: false}
console.log(g7.throw(new Error('Oops'))); // Logs: "Caught: Oops"
// Returns: {value: 'error handled', done: false}
console.log(g7.next()); // {value: 4, done: false}
console.log(g7.next()); // {value: undefined, done: true}
// Uncaught error
function* gen8() {
yield 1;
yield 2; // No try-catch
yield 3;
}
const g8 = gen8();
console.log(g8.next()); // {value: 1, done: false}
try {
g8.throw(new Error('Uncaught'));
} catch (e) {
console.log('Exception propagated:', e.message);
// "Exception propagated: Uncaught"
}
console.log(g8.next()); // {value: undefined, done: true} (generator closed)
// finally with throw
function* gen9() {
try {
yield 1;
yield 2;
} finally {
console.log('Cleanup after error');
}
}
const g9 = gen9();
console.log(g9.next()); // {value: 1, done: false}
try {
g9.throw(new Error('Error'));
} catch (e) {
console.log('Caught outside');
// Logs: "Cleanup after error" (finally runs)
// Logs: "Caught outside"
}
Example: Cleanup with finally
// Resource management
function* databaseQuery() {
console.log('Opening connection');
try {
yield 'row 1';
yield 'row 2';
yield 'row 3';
} finally {
console.log('Closing connection');
}
}
// Normal completion
console.log('--- Normal completion ---');
for (const row of databaseQuery()) {
console.log(row);
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "row 3"
// "Closing connection"
// Early termination
console.log('--- Early termination ---');
const query = databaseQuery();
console.log(query.next().value); // "Opening connection", "row 1"
console.log(query.next().value); // "row 2"
query.return(); // "Closing connection"
// Break in for...of
console.log('--- Break in loop ---');
for (const row of databaseQuery()) {
console.log(row);
if (row === 'row 2') break; // Triggers cleanup
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "Closing connection"
// Error handling
console.log('--- Error ---');
function* withCleanup() {
console.log('Setup');
try {
yield 1;
throw new Error('Internal error');
yield 2; // Never reached
} catch (e) {
console.log('Error in generator:', e.message);
} finally {
console.log('Cleanup');
}
}
for (const value of withCleanup()) {
console.log(value);
}
// Logs:
// "Setup"
// "1"
// "Error in generator: Internal error"
// "Cleanup"
Example: Return values and yield*
// Return value from generator
function* inner() {
yield 1;
yield 2;
return 'inner done';
}
function* outer1() {
const result = yield* inner();
console.log('Inner returned:', result);
yield 3;
}
console.log([...outer1()]);
// Logs: "Inner returned: inner done"
// Returns: [1, 2, 3]
// Return value not included in iteration
function* gen10() {
yield 1;
yield 2;
return 42;
}
console.log([...gen10()]); // [1, 2] (42 not included)
// But captured by yield*
function* outer2() {
const returnValue = yield* gen10();
console.log('Got return value:', returnValue);
yield returnValue;
}
console.log([...outer2()]);
// Logs: "Got return value: 42"
// Returns: [1, 2, 42]
// Chaining generators
function* pipeline() {
const r1 = yield* stage1();
console.log('Stage 1 result:', r1);
const r2 = yield* stage2(r1);
console.log('Stage 2 result:', r2);
return r2;
}
function* stage1() {
yield 'processing...';
return 'stage1-complete';
}
function* stage2(input) {
yield `using ${input}`;
return 'stage2-complete';
}
console.log([...pipeline()]);
// Logs: "Stage 1 result: stage1-complete"
// Logs: "Stage 2 result: stage2-complete"
// Returns: ["processing...", "using stage1-complete"]
// Final result
const gen11 = pipeline();
let result;
while (true) {
result = gen11.next();
if (result.done) {
console.log('Final return:', result.value); // "stage2-complete"
break;
}
}
Important:
return() terminates generator and runs finally blocks.
throw()
throws error at yield point (must be caught or generator closes). Return values from generators not included in iteration but captured by yield*. Use finally for
cleanup. Early break in for...of triggers cleanup via return().
18.4 Async Generators and for-await-of Loops
Async Generator Syntax
| Type | Syntax | Returns |
|---|---|---|
| Function Declaration | async function* name() {} |
AsyncGenerator object |
| Function Expression | const f = async function*() {} |
AsyncGenerator object |
| Method | async *method() {} |
AsyncGenerator object |
| Class Method | async *method() {} |
AsyncGenerator object |
AsyncGenerator Methods
| Method | Syntax | Description | Returns |
|---|---|---|---|
| next() | await gen.next(value) |
Resume, send value | Promise<{value, done}> |
| return() | await gen.return(value) |
Terminate generator | Promise<{value, done: true}> |
| throw() | await gen.throw(error) |
Throw error at yield | Promise<{value, done}> |
for-await-of Loop
| Feature | Syntax | Works With |
|---|---|---|
| Basic | for await (const x of asyncIterable) |
Async iterables |
| Await promises | Automatically awaits each value | AsyncGenerator, async iterables |
| Context | Must be in async function | Any async function or top-level |
| Break/Continue | Supported | Same as regular for...of |
Async Iterable Protocol
| Requirement | Description | Returns |
|---|---|---|
| [Symbol.asyncIterator] | Method returning async iterator | Object with next() returning Promise |
| next() method | Returns promise of {value, done} | Promise<{value: any, done: boolean}> |
Example: Basic async generators
// Simple async generator
async function* asyncNumbers() {
yield 1;
yield 2;
yield 3;
}
// Consume with for-await-of
async function main1() {
for await (const num of asyncNumbers()) {
console.log(num); // 1, 2, 3
}
}
main1();
// Async generator with delays
async function* delayedSequence() {
yield await Promise.resolve(1);
await new Promise(resolve => setTimeout(resolve, 100));
yield 2;
await new Promise(resolve => setTimeout(resolve, 100));
yield 3;
}
async function main2() {
console.log('Start');
for await (const value of delayedSequence()) {
console.log(value); // 1 (then 100ms), 2 (then 100ms), 3
}
console.log('Done');
}
main2();
// Fetch data in chunks
async function* fetchPages(baseUrl, maxPages) {
for (let page = 1; page <= maxPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data;
}
}
async function processAllPages() {
for await (const pageData of fetchPages('/api/items', 5)) {
console.log('Processing page:', pageData);
// Process page data
}
}
Example: Async generator methods
// Manual iteration
async function* asyncGen() {
yield 1;
yield 2;
yield 3;
}
async function manual() {
const gen = asyncGen();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.next()); // {value: 2, done: false}
console.log(await gen.next()); // {value: 3, done: false}
console.log(await gen.next()); // {value: undefined, done: true}
}
manual();
// Early termination with return()
async function* withCleanup() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('Cleanup');
}
}
async function earlyExit() {
const gen = withCleanup();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.return(99)); // Logs: "Cleanup"
// Returns: {value: 99, done: true}
console.log(await gen.next()); // {value: undefined, done: true}
}
earlyExit();
// Error handling with throw()
async function* withErrorHandling() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e.message);
yield 'recovered';
}
}
async function throwError() {
const gen = withErrorHandling();
console.log(await gen.next()); // {value: 1, done: false}
console.log(await gen.throw(new Error('Oops'))); // Logs: "Caught: Oops"
// Returns: {value: 'recovered', done: false}
console.log(await gen.next()); // {value: undefined, done: true}
}
throwError();
Example: Streaming data processing
// Read file in chunks
async function* readFileInChunks(filename, chunkSize = 1024) {
// Simulated file reading (in real code, use fs.createReadStream)
const fileContent = 'Large file content...';
for (let i = 0; i < fileContent.length; i += chunkSize) {
const chunk = fileContent.slice(i, i + chunkSize);
yield chunk;
// Simulate async I/O
await new Promise(resolve => setTimeout(resolve, 10));
}
}
async function processFile() {
for await (const chunk of readFileInChunks('data.txt', 100)) {
console.log('Processing chunk:', chunk.length, 'bytes');
}
}
// Event stream
async function* eventStream(eventEmitter, eventName) {
const queue = [];
let resolveNext;
const handler = (data) => {
if (resolveNext) {
resolveNext(data);
resolveNext = null;
} else {
queue.push(data);
}
};
eventEmitter.on(eventName, handler);
try {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else {
yield await new Promise(resolve => {
resolveNext = resolve;
});
}
}
} finally {
eventEmitter.off(eventName, handler);
}
}
// Usage with EventEmitter
async function processEvents(emitter) {
for await (const event of eventStream(emitter, 'data')) {
console.log('Event:', event);
if (event.type === 'end') break;
}
}
// WebSocket stream
async function* websocketStream(url) {
const ws = new WebSocket(url);
const queue = [];
let resolveNext;
let done = false;
ws.onmessage = (event) => {
if (resolveNext) {
resolveNext({value: event.data, done: false});
resolveNext = null;
} else {
queue.push(event.data);
}
};
ws.onclose = () => {
done = true;
if (resolveNext) {
resolveNext({done: true});
}
};
try {
while (!done) {
if (queue.length > 0) {
yield queue.shift();
} else {
const result = await new Promise(resolve => {
resolveNext = resolve;
});
if (result.done) break;
yield result.value;
}
}
} finally {
ws.close();
}
}
async function processWebSocketMessages() {
for await (const message of websocketStream('ws://example.com')) {
console.log('Message:', message);
}
}
Example: Combining async generators
// Merge multiple async streams
async function* merge(...asyncIterables) {
const iterators = asyncIterables.map(it => it[Symbol.asyncIterator]());
const promises = new Map(
iterators.map((it, i) => [i, it.next().then(result => ({i, result}))])
);
while (promises.size > 0) {
const {i, result} = await Promise.race(promises.values());
if (result.done) {
promises.delete(i);
} else {
yield result.value;
promises.set(i,
iterators[i].next().then(result => ({i, result}))
);
}
}
}
async function* source1() {
yield 1;
await new Promise(r => setTimeout(r, 100));
yield 2;
}
async function* source2() {
yield 'a';
await new Promise(r => setTimeout(r, 50));
yield 'b';
}
async function testMerge() {
for await (const value of merge(source1(), source2())) {
console.log(value); // Order depends on timing: a, 1, b, 2 or a, b, 1, 2
}
}
// Transform async stream
async function* map(fn, asyncIterable) {
for await (const value of asyncIterable) {
yield fn(value);
}
}
async function* filter(predicate, asyncIterable) {
for await (const value of asyncIterable) {
if (predicate(value)) {
yield value;
}
}
}
async function* numbers() {
for (let i = 1; i <= 10; i++) {
yield i;
await new Promise(r => setTimeout(r, 10));
}
}
async function processNumbers() {
const evens = filter(n => n % 2 === 0, numbers());
const doubled = map(n => n * 2, evens);
for await (const value of doubled) {
console.log(value); // 4, 8, 12, 16, 20
}
}
// Throttle async stream
async function* throttle(asyncIterable, delay) {
for await (const value of asyncIterable) {
yield value;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
async function throttleExample() {
for await (const num of throttle(numbers(), 100)) {
console.log(num); // 1, (100ms), 2, (100ms), 3, ...
}
}
Key Points:
async function* creates async generator. Use
for await...of
to iterate (must be in async context). Methods return promises: await gen.next(). Perfect for
streaming data, event processing, paginated APIs. AsyncGenerator is both async
iterable and async iterator.
18.5 Custom Iterator Implementation
Iterator Implementation Checklist
| Step | Requirement | Implementation |
|---|---|---|
| 1. Make iterable | Add [Symbol.iterator] method | Returns iterator object |
| 2. Return iterator | Iterator has next() method | Returns {value, done} |
| 3. Track state | Maintain iteration position | Use closure or object properties |
| 4. Signal completion | Set done: true when finished | Return {value: undefined, done: true} |
| 5. Optional: return/throw | Add cleanup support | Implement return() and throw() methods |
Iterator Pattern Variations
| Pattern | When to Use | Example |
|---|---|---|
| Simple Iterator | Single iteration, no cleanup | Range, sequence |
| Reusable Iterator | Multiple iterations from same object | Array, collection |
| Stateful Iterator | Complex state management | Tree traversal, pagination |
| Iterator with Cleanup | Resource management needed | File reader, DB cursor |
| Infinite Iterator | Unbounded sequences | Counter, random generator |
Example: Custom collection iterators
// Simple range iterator
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) {
console.log(n); // 1, 3, 5, 7, 9
}
console.log([...range]); // [1, 3, 5, 7, 9]
// Multiple independent iterations
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();
console.log(iter1.next().value); // 1
console.log(iter2.next().value); // 1 (independent)
console.log(iter1.next().value); // 3
console.log(iter2.next().value); // 3
// Linked list iterator
class Node {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
append(value) {
const node = new Node(value);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.size++;
}
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return {value, done: false};
}
return {value: undefined, done: true};
}
};
}
}
const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);
for (const value of list) {
console.log(value); // 10, 20, 30
}
// Map and filter on custom iterable
const doubled = [...list].map(x => x * 2);
console.log(doubled); // [20, 40, 60]
Example: Iterators with cleanup
// Iterator with return() method
class FileReader {
constructor(filename) {
this.filename = filename;
}
[Symbol.iterator]() {
let file = this.openFile(this.filename);
let lineNumber = 0;
return {
next() {
const line = this.readLine(file);
if (line !== null) {
return {
value: {lineNumber: ++lineNumber, text: line},
done: false
};
}
this.closeFile(file);
return {value: undefined, done: true};
}.bind(this),
return(value) {
// Cleanup on early termination
console.log('Closing file early');
this.closeFile(file);
return {value, done: true};
}.bind(this)
};
}
openFile(filename) {
console.log('Opening file:', filename);
return {/* file handle */};
}
readLine(file) {
// Simulated
return null;
}
closeFile(file) {
console.log('Closing file');
}
}
const reader = new FileReader('data.txt');
// Early break triggers return()
for (const line of reader) {
console.log(line);
if (line.lineNumber === 5) break; // Calls return()
}
// Logs: "Closing file early"
// Iterator with throw() support
class SafeIterator {
constructor(items) {
this.items = items;
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
let errorHandled = false;
return {
next() {
if (errorHandled) {
return {value: undefined, done: true};
}
if (index < items.length) {
return {value: items[index++], done: false};
}
return {value: undefined, done: true};
},
throw(error) {
console.log('Error in iterator:', error.message);
errorHandled = true;
return {value: 'error-handled', done: false};
},
return(value) {
console.log('Cleanup');
return {value, done: true};
}
};
}
}
const safe = new SafeIterator([1, 2, 3]);
const safeIter = safe[Symbol.iterator]();
console.log(safeIter.next()); // {value: 1, done: false}
console.log(safeIter.throw(new Error('Oops'))); // Logs: "Error in iterator: Oops"
// Returns: {value: 'error-handled', done: false}
console.log(safeIter.next()); // {value: undefined, done: true}
Example: Advanced iterator patterns
// Bidirectional iterator
class BidirectionalList {
constructor(items) {
this.items = items;
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
return {
next() {
if (index < items.length) {
return {value: items[index++], done: false};
}
return {value: undefined, done: true};
},
// Custom method for backward iteration
prev() {
if (index > 0) {
return {value: items[--index], done: false};
}
return {value: undefined, done: true};
}
};
}
}
const biList = new BidirectionalList([1, 2, 3, 4, 5]);
const biIter = biList[Symbol.iterator]();
console.log(biIter.next().value); // 1
console.log(biIter.next().value); // 2
console.log(biIter.next().value); // 3
console.log(biIter.prev().value); // 2
console.log(biIter.prev().value); // 1
console.log(biIter.next().value); // 2
// Peekable iterator
class PeekableIterator {
constructor(iterable) {
this.iterator = iterable[Symbol.iterator]();
this.peeked = null;
this.hasPeeked = false;
}
next() {
if (this.hasPeeked) {
this.hasPeeked = false;
return this.peeked;
}
return this.iterator.next();
}
peek() {
if (!this.hasPeeked) {
this.peeked = this.iterator.next();
this.hasPeeked = true;
}
return this.peeked;
}
[Symbol.iterator]() {
return this;
}
}
const peekable = new PeekableIterator([1, 2, 3, 4, 5]);
console.log(peekable.peek().value); // 1 (doesn't advance)
console.log(peekable.peek().value); // 1 (still doesn't advance)
console.log(peekable.next().value); // 1 (now advances)
console.log(peekable.next().value); // 2
console.log(peekable.peek().value); // 3
console.log(peekable.next().value); // 3
// Cycling iterator
class CycleIterator {
constructor(iterable) {
this.iterable = iterable;
this.iterator = null;
}
[Symbol.iterator]() {
return this;
}
next() {
if (!this.iterator) {
this.iterator = this.iterable[Symbol.iterator]();
}
const result = this.iterator.next();
if (result.done) {
// Restart
this.iterator = this.iterable[Symbol.iterator]();
return this.iterator.next();
}
return result;
}
}
const cycle = new CycleIterator([1, 2, 3]);
console.log(cycle.next().value); // 1
console.log(cycle.next().value); // 2
console.log(cycle.next().value); // 3
console.log(cycle.next().value); // 1 (cycles back)
console.log(cycle.next().value); // 2
console.log(cycle.next().value); // 3
console.log(cycle.next().value); // 1 (cycles again)
Key Points: Implement
[Symbol.iterator]() returning object with
next().
Track state with closure or properties. Optional: return() for cleanup, throw() for
error handling. Each [Symbol.iterator]() call should return new independent
iterator. Use generators for simpler implementation.
18.6 Iterator Helper Methods (take, drop, filter)
Iterator Helper Methods (ES2024+) New
| Method | Syntax | Description | Returns |
|---|---|---|---|
| take() | iter.take(n) |
Take first n elements | Iterator |
| drop() | iter.drop(n) |
Skip first n elements | Iterator |
| filter() | iter.filter(predicate) |
Filter elements by predicate | Iterator |
| map() | iter.map(fn) |
Transform each element | Iterator |
| flatMap() | iter.flatMap(fn) |
Map and flatten one level | Iterator |
| reduce() | iter.reduce(fn, init) |
Reduce to single value | Any |
| forEach() | iter.forEach(fn) |
Execute function for each element | undefined |
| some() | iter.some(predicate) |
Test if any element matches | Boolean |
| every() | iter.every(predicate) |
Test if all elements match | Boolean |
| find() | iter.find(predicate) |
Find first matching element | Any or undefined |
| toArray() | iter.toArray() |
Convert to array | Array |
Lazy vs Eager Evaluation
| Category | Methods | Evaluation | Chainable |
|---|---|---|---|
| Lazy | take, drop, filter, map, flatMap | On demand, returns iterator | ✓ Yes |
| Eager | reduce, forEach, some, every, find, toArray | Immediate, returns value | ✗ No (terminal) |
Example: Polyfill implementations (pre-ES2024)
// Helper method polyfills for older environments
function* take(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) break;
yield value;
}
}
function* drop(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count++ >= n) {
yield value;
}
}
}
function* filter(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
function* map(fn, iterable) {
for (const value of iterable) {
yield fn(value);
}
}
function* flatMap(fn, iterable) {
for (const value of iterable) {
yield* fn(value);
}
}
// Usage examples
function* naturals() {
let n = 1;
while (true) yield n++;
}
// Take first 5 numbers
const first5 = [...take(5, naturals())];
console.log(first5); // [1, 2, 3, 4, 5]
// Skip first 10, take next 5
const numbers = [...take(5, drop(10, naturals()))];
console.log(numbers); // [11, 12, 13, 14, 15]
// Filter even numbers
const evens = filter(n => n % 2 === 0, naturals());
console.log([...take(5, evens)]); // [2, 4, 6, 8, 10]
// Chain transformations (lazy evaluation)
const result = take(5,
map(n => n * n,
filter(n => n % 2 === 0, naturals())
)
);
console.log([...result]); // [4, 16, 36, 64, 100]
// FlatMap example
function* duplicateEach(n) {
yield n;
yield n;
}
const duplicated = flatMap(duplicateEach, [1, 2, 3]);
console.log([...duplicated]); // [1, 1, 2, 2, 3, 3]
Example: Terminal operations
// Reduce
function reduce(fn, initial, iterable) {
let accumulator = initial;
for (const value of iterable) {
accumulator = fn(accumulator, value);
}
return accumulator;
}
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const sum = reduce((acc, n) => acc + n, 0, range(1, 10));
console.log(sum); // 55
// forEach (side effects)
function forEach(fn, iterable) {
for (const value of iterable) {
fn(value);
}
}
forEach(n => console.log(n), range(1, 5));
// Logs: 1, 2, 3, 4, 5
// some
function some(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
return true;
}
}
return false;
}
console.log(some(n => n > 5, range(1, 10))); // true
console.log(some(n => n > 20, range(1, 10))); // false
// every
function every(predicate, iterable) {
for (const value of iterable) {
if (!predicate(value)) {
return false;
}
}
return true;
}
console.log(every(n => n > 0, range(1, 10))); // true
console.log(every(n => n < 5, range(1, 10))); // false
// find
function find(predicate, iterable) {
for (const value of iterable) {
if (predicate(value)) {
return value;
}
}
return undefined;
}
console.log(find(n => n > 5, range(1, 10))); // 6
console.log(find(n => n > 20, range(1, 10))); // undefined
// toArray
function toArray(iterable) {
return [...iterable];
}
console.log(toArray(range(1, 5))); // [1, 2, 3, 4, 5]
Example: Practical use cases
// Process large dataset lazily
function* readLargeFile() {
// Simulate reading millions of lines
for (let i = 1; i <= 1000000; i++) {
yield `Line ${i}`;
}
}
// Only processes first 100 matching lines (lazy!)
const validLines = take(100,
filter(line => line.includes('error'),
map(line => line.toLowerCase(),
readLargeFile()
)
)
);
// No processing until we iterate
for (const line of validLines) {
console.log(line);
}
// Pagination helper
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
const data = [...range(1, 100)];
const pages = paginate(data, 10);
// Get specific page
const page3 = [...take(1, drop(2, pages))][0];
console.log(page3); // [21, 22, 23, ..., 30]
// Sliding window
function* window(size, iterable) {
const buffer = [];
for (const value of iterable) {
buffer.push(value);
if (buffer.length === size) {
yield [...buffer];
buffer.shift();
}
}
}
const windows = [...window(3, [1, 2, 3, 4, 5])];
console.log(windows);
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
// Batch processing
function* batch(size, iterable) {
let batch = [];
for (const value of iterable) {
batch.push(value);
if (batch.length === size) {
yield batch;
batch = [];
}
}
if (batch.length > 0) {
yield batch;
}
}
const batches = [...batch(3, range(1, 10))];
console.log(batches);
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
// Zip iterators
function* zip(...iterables) {
const iterators = iterables.map(it => it[Symbol.iterator]());
while (true) {
const results = iterators.map(it => it.next());
if (results.some(r => r.done)) {
break;
}
yield results.map(r => r.value);
}
}
const zipped = [...zip([1, 2, 3], ['a', 'b', 'c'], [true, false, true])];
console.log(zipped);
// [[1, 'a', true], [2, 'b', false], [3, 'c', true]]
// Enumerate (with index)
function* enumerate(iterable) {
let index = 0;
for (const value of iterable) {
yield [index++, value];
}
}
const enumerated = [...enumerate(['a', 'b', 'c'])];
console.log(enumerated);
// [[0, 'a'], [1, 'b'], [2, 'c']]
Example: Composing iterator helpers
// Create reusable iterator pipelines
class IteratorPipeline {
constructor(iterable) {
this.iterable = iterable;
}
take(n) {
this.iterable = take(n, this.iterable);
return this;
}
drop(n) {
this.iterable = drop(n, this.iterable);
return this;
}
filter(predicate) {
this.iterable = filter(predicate, this.iterable);
return this;
}
map(fn) {
this.iterable = map(fn, this.iterable);
return this;
}
flatMap(fn) {
this.iterable = flatMap(fn, this.iterable);
return this;
}
// Terminal operations
toArray() {
return [...this.iterable];
}
reduce(fn, initial) {
return reduce(fn, initial, this.iterable);
}
forEach(fn) {
forEach(fn, this.iterable);
}
some(predicate) {
return some(predicate, this.iterable);
}
every(predicate) {
return every(predicate, this.iterable);
}
find(predicate) {
return find(predicate, this.iterable);
}
}
// Helper function
function pipeline(iterable) {
return new IteratorPipeline(iterable);
}
// Usage
const result1 = pipeline(naturals())
.filter(n => n % 2 === 0)
.map(n => n ** 2)
.take(5)
.toArray();
console.log(result1); // [4, 16, 36, 64, 100]
// Complex pipeline
const result2 = pipeline(range(1, 100))
.filter(n => n % 3 === 0)
.drop(5)
.map(n => n * 2)
.take(10)
.reduce((sum, n) => sum + n, 0);
console.log(result2); // Sum of processed values
// Find first match
const found = pipeline(naturals())
.filter(n => n % 7 === 0)
.find(n => n > 100);
console.log(found); // 105
// Test condition
const hasLarge = pipeline(range(1, 10))
.map(n => n ** 2)
.some(n => n > 50);
console.log(hasLarge); // true
Key Points: Iterator helpers enable functional programming
patterns
with lazy evaluation. Lazy methods (take, drop, filter, map) return iterators. Eager methods (reduce, forEach,
some) consume iterator immediately. Chain helpers for complex transformations. Perfect for
large datasets - only processes needed elements. ES2024+ adds native support.
Section 18 Summary: Iterators, Generators, and Iteration Protocols
- Iterator Protocol: Object with next() returning {value, done}
- Iterable Protocol: Object with [Symbol.iterator] method
- Built-in Iterables: Array, String, Map, Set, TypedArray, arguments
- Generators: function* syntax, pausable execution with yield
- yield Expressions: yield value, yield* delegation, two-way communication
- Generator Methods: next(), return(), throw()
- Async Generators: async function*, for await...of loops
- Custom Iterators: Implement [Symbol.iterator], track state, optional cleanup
- Iterator Helpers: take, drop, filter, map, reduce (lazy evaluation)
- Lazy Evaluation: Process data on demand, efficient for infinite sequences
- Use Cases: Streaming data, pagination, infinite sequences, resource management
19. Error Handling and Debugging Techniques
19.1 Error Types and Error Object Properties
Built-in Error Types
| Error Type | Description | Common Causes |
|---|---|---|
| Error | Generic error (base class) | Manually thrown errors |
| SyntaxError | Invalid JavaScript syntax | Parse errors, malformed code |
| ReferenceError | Invalid reference to variable | Undefined variables, accessing before declaration |
| TypeError | Value is not expected type | Calling non-function, accessing property on null/undefined |
| RangeError | Value not in allowed range | Invalid array length, recursion limit |
| URIError | Invalid URI encoding/decoding | encodeURI/decodeURI with malformed URI |
| EvalError | Error in eval() (legacy) | Rarely used in modern JavaScript |
| AggregateError ES2021 | Multiple errors wrapped together | Promise.any() rejection, multiple failures |
Error Object Properties
| Property | Description | Standard |
|---|---|---|
| name | Error type name (e.g., "TypeError") | ✓ Standard |
| message | Human-readable error description | ✓ Standard |
| stack | Stack trace (non-standard but widely supported) | ✗ Non-standard |
| cause ES2022 | Underlying error that caused this error | ✓ Standard |
| fileName | File where error occurred (Firefox) | ✗ Non-standard |
| lineNumber | Line number where error occurred (Firefox) | ✗ Non-standard |
| columnNumber | Column number where error occurred (Firefox) | ✗ Non-standard |
Error Constructor Options
| Constructor | Syntax | Parameters |
|---|---|---|
| new Error() | new Error(message, options) |
message: string, options: {cause} |
| message | Error description | String |
| options.cause | Original error that caused this error | Any (typically Error object) |
Example: Built-in error types
// SyntaxError (parse-time error, not catchable at runtime)
// eval('var x = ;'); // SyntaxError: Unexpected token ';'
// ReferenceError
try {
console.log(nonExistentVariable);
} catch (e) {
console.log(e.name); // "ReferenceError"
console.log(e.message); // "nonExistentVariable is not defined"
}
// TypeError
try {
const obj = null;
obj.someMethod();
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "Cannot read properties of null"
}
try {
const notAFunction = 42;
notAFunction();
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "notAFunction is not a function"
}
// RangeError
try {
const arr = new Array(-1);
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "Invalid array length"
}
function recursiveFunction() {
recursiveFunction();
}
try {
recursiveFunction();
} catch (e) {
console.log(e.name); // "RangeError"
console.log(e.message); // "Maximum call stack size exceeded"
}
// URIError
try {
decodeURIComponent('%');
} catch (e) {
console.log(e.name); // "URIError"
console.log(e.message); // "URI malformed"
}
// AggregateError (ES2021)
const errors = [
new Error('Error 1'),
new Error('Error 2'),
new Error('Error 3')
];
const aggregateError = new AggregateError(errors, 'Multiple errors occurred');
console.log(aggregateError.name); // "AggregateError"
console.log(aggregateError.message); // "Multiple errors occurred"
console.log(aggregateError.errors); // Array of 3 errors
aggregateError.errors.forEach((err, i) => {
console.log(`Error ${i + 1}:`, err.message);
});
Example: Error object properties
// Creating error manually
const error = new Error('Something went wrong');
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace (multi-line string)
// Stack trace example
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error('Error in function c');
}
try {
a();
} catch (e) {
console.log(e.stack);
// Error: Error in function c
// at c (file.js:10:11)
// at b (file.js:6:5)
// at a (file.js:2:5)
// at file.js:14:5
}
// Error cause (ES2022)
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (originalError) {
// Wrap original error with context
throw new Error('Failed to fetch data', {
cause: originalError
});
}
}
try {
await fetchData('https://invalid-url');
} catch (error) {
console.log(error.message); // "Failed to fetch data"
console.log(error.cause); // Original fetch error
console.log(error.cause.message); // Original error message
}
// Error with custom properties
const customError = new Error('Custom error');
customError.code = 'ERR_CUSTOM';
customError.statusCode = 400;
customError.timestamp = Date.now();
console.log(customError.code); // "ERR_CUSTOM"
console.log(customError.statusCode); // 400
console.log(customError.timestamp); // Unix timestamp
Example: Parsing stack traces
// Parse stack trace for debugging
function parseStackTrace(error) {
if (!error.stack) return [];
const lines = error.stack.split('\n');
const frames = [];
// Skip first line (error message)
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
// Example format: "at functionName (file.js:10:5)"
const match = line.match(/at (.+?) \((.+?):(\d+):(\d+)\)/);
if (match) {
frames.push({
functionName: match[1],
fileName: match[2],
lineNumber: parseInt(match[3], 10),
columnNumber: parseInt(match[4], 10)
});
}
}
return frames;
}
try {
throw new Error('Test error');
} catch (e) {
const frames = parseStackTrace(e);
console.log('Stack frames:', frames);
// Print formatted stack
frames.forEach((frame, i) => {
console.log(
` ${i + 1}. ${frame.functionName}`,
`at ${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`
);
});
}
// Capture stack trace programmatically
function captureStack() {
const obj = {};
Error.captureStackTrace(obj, captureStack);
return obj.stack;
}
console.log(captureStack());
Key Points: JavaScript has 8 built-in error types. All errors
have
name and message properties. stack property (non-standard but
universal) provides call stack. ES2022 cause option enables error
chaining.
AggregateError (ES2021) wraps multiple errors.
19.2 Custom Error Classes and Error Inheritance
Custom Error Pattern
| Step | Requirement | Implementation |
|---|---|---|
| 1. Extend Error | Inherit from Error class | class CustomError extends Error |
| 2. Set name | Set error name to class name | this.name = this.constructor.name |
| 3. Call super() | Initialize Error with message | super(message) |
| 4. Capture stack | Fix stack trace (optional) | Error.captureStackTrace(this, this.constructor) |
| 5. Add properties | Add custom data | this.code = code |
Common Custom Error Types
| Error Type | Use Case | Additional Properties |
|---|---|---|
| ValidationError | Input validation failures | field, value, rule |
| NotFoundError | Resource not found (404) | resourceType, resourceId |
| UnauthorizedError | Authentication failure (401) | userId, reason |
| ForbiddenError | Authorization failure (403) | action, resource |
| NetworkError | Network/HTTP failures | statusCode, url, method |
| TimeoutError | Operation timeout | duration, operation |
| DatabaseError | Database operation failures | query, code |
Example: Basic custom error class
// Simple custom error
class CustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
// Maintains proper stack trace (V8 only)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Usage
try {
throw new CustomError('Something went wrong');
} catch (e) {
console.log(e instanceof CustomError); // true
console.log(e instanceof Error); // true
console.log(e.name); // "CustomError"
console.log(e.message); // "Something went wrong"
}
// Custom error with additional properties
class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.value = value;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
function validateAge(age) {
if (typeof age !== 'number') {
throw new ValidationError(
'Age must be a number',
'age',
age
);
}
if (age < 0 || age > 150) {
throw new ValidationError(
'Age must be between 0 and 150',
'age',
age
);
}
return true;
}
try {
validateAge('twenty');
} catch (e) {
if (e instanceof ValidationError) {
console.log(`Validation failed for field "${e.field}"`);
console.log(`Invalid value: ${e.value}`);
console.log(`Error: ${e.message}`);
}
}
try {
validateAge(200);
} catch (e) {
console.log(e.field); // "age"
console.log(e.value); // 200
console.log(e.message); // "Age must be between 0 and 150"
}
Example: Domain-specific error classes
// HTTP error hierarchy
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'HttpError';
this.statusCode = statusCode;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class NotFoundError extends HttpError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 404);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
}
}
class UnauthorizedError extends HttpError {
constructor(message = 'Unauthorized') {
super(message, 401);
this.name = 'UnauthorizedError';
}
}
class ForbiddenError extends HttpError {
constructor(action, resource) {
super(`Not allowed to ${action} ${resource}`, 403);
this.name = 'ForbiddenError';
this.action = action;
this.resource = resource;
}
}
// Usage
async function getUser(userId) {
const user = await db.findUser(userId);
if (!user) {
throw new NotFoundError('User', userId);
}
return user;
}
async function deletePost(userId, postId) {
const post = await db.findPost(postId);
if (!post) {
throw new NotFoundError('Post', postId);
}
if (post.authorId !== userId) {
throw new ForbiddenError('delete', 'post');
}
await db.deletePost(postId);
}
// Error handling
try {
await getUser(999);
} catch (e) {
if (e instanceof NotFoundError) {
console.log(`Status: ${e.statusCode}`);
console.log(`${e.resource} ${e.id} not found`);
} else if (e instanceof HttpError) {
console.log(`HTTP Error ${e.statusCode}: ${e.message}`);
}
}
Example: Advanced error patterns
// Error with error codes
class ApplicationError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'ApplicationError';
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
details: this.details,
timestamp: this.timestamp
};
}
}
// Error factory
class ErrorFactory {
static validation(field, message) {
return new ApplicationError(
message,
'VALIDATION_ERROR',
{field}
);
}
static notFound(resource, id) {
return new ApplicationError(
`${resource} not found`,
'NOT_FOUND',
{resource, id}
);
}
static network(url, statusCode) {
return new ApplicationError(
'Network request failed',
'NETWORK_ERROR',
{url, statusCode}
);
}
}
try {
throw ErrorFactory.validation('email', 'Invalid email format');
} catch (e) {
console.log(e.code); // "VALIDATION_ERROR"
console.log(e.details.field); // "email"
console.log(JSON.stringify(e)); // Full error as JSON
}
// Async error wrapper
class AsyncOperationError extends Error {
constructor(message, operation, originalError) {
super(message);
this.name = 'AsyncOperationError';
this.operation = operation;
this.originalError = originalError;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
async function wrapAsync(operation, fn) {
try {
return await fn();
} catch (error) {
throw new AsyncOperationError(
`Failed to ${operation}`,
operation,
error
);
}
}
try {
await wrapAsync('fetch user data', async () => {
throw new Error('Network timeout');
});
} catch (e) {
console.log(e.message); // "Failed to fetch user data"
console.log(e.operation); // "fetch user data"
console.log(e.originalError.message); // "Network timeout"
}
// Retryable error
class RetryableError extends Error {
constructor(message, retryAfter = 1000) {
super(message);
this.name = 'RetryableError';
this.retryAfter = retryAfter;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fetch(url);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
throw new RetryableError(
`Attempt ${attempt} failed`,
1000 * attempt
);
}
}
}
Best Practices: Extend
Error class for custom errors. Set name
property to class name. Use Error.captureStackTrace for clean stack traces. Add domain-specific
properties (field, statusCode, code). Create error hierarchies for different
error categories. Implement toJSON() for serialization.
19.3 Exception Propagation and Error Boundaries
Exception Propagation Rules
| Context | Behavior | Handling |
|---|---|---|
| Synchronous code | Propagates up call stack | try-catch in caller |
| Promise chain | Propagates through .then() chain | .catch() or try-catch with await |
| Async function | Returns rejected promise | try-catch or .catch() |
| Event handlers | Does not propagate to caller | Handle in event handler or window.onerror |
| Callbacks | Does not propagate to caller | Error-first callback pattern |
| setTimeout/setInterval | Does not propagate | Handle in callback or window.onerror |
Error Boundary Patterns
| Pattern | Scope | Use Case |
|---|---|---|
| try-catch | Block-level | Synchronous code, async/await |
| .catch() | Promise chain | Promise-based operations |
| finally | Cleanup after try-catch | Resource cleanup (always runs) |
| window.onerror | Global (window) | Uncaught exceptions |
| window.onunhandledrejection | Global (promises) | Unhandled promise rejections |
| process.on('uncaughtException') | Global (Node.js) | Uncaught exceptions in Node |
| process.on('unhandledRejection') | Global (Node.js promises) | Unhandled rejections in Node |
Error Recovery Strategies
| Strategy | Description | When to Use |
|---|---|---|
| Fail fast | Throw immediately, don't catch | Programming errors, invalid state |
| Retry | Attempt operation again | Transient failures (network, rate limits) |
| Fallback | Use alternative approach/data | Non-critical features, degraded mode |
| Circuit breaker | Stop trying after repeated failures | External service failures |
| Log and continue | Record error but don't stop | Non-critical operations |
| Graceful degradation | Reduce functionality but keep working | Feature failures in production |
Example: Exception propagation patterns
// Synchronous propagation
function level3() {
throw new Error('Error at level 3');
}
function level2() {
level3(); // Error propagates up
}
function level1() {
try {
level2();
} catch (e) {
console.log('Caught at level 1:', e.message);
}
}
level1(); // "Caught at level 1: Error at level 3"
// Async propagation with promises
function asyncLevel3() {
return Promise.reject(new Error('Async error'));
}
function asyncLevel2() {
return asyncLevel3(); // Rejection propagates
}
function asyncLevel1() {
return asyncLevel2()
.catch(e => {
console.log('Caught in promise chain:', e.message);
});
}
asyncLevel1();
// Async/await propagation
async function asyncAwaitLevel3() {
throw new Error('Async/await error');
}
async function asyncAwaitLevel2() {
await asyncAwaitLevel3(); // Error propagates
}
async function asyncAwaitLevel1() {
try {
await asyncAwaitLevel2();
} catch (e) {
console.log('Caught with async/await:', e.message);
}
}
asyncAwaitLevel1();
// Event handler - no propagation
document.getElementById('button').addEventListener('click', () => {
throw new Error('Error in event handler');
// This will NOT be caught by outer try-catch
});
// Must handle in event handler
document.getElementById('button').addEventListener('click', () => {
try {
// risky operation
throw new Error('Handled error');
} catch (e) {
console.log('Caught in handler:', e.message);
}
});
// Callback - no propagation
function asyncOperation(callback) {
setTimeout(() => {
try {
throw new Error('Error in callback');
} catch (e) {
callback(e, null); // Error-first callback pattern
}
}, 100);
}
asyncOperation((error, result) => {
if (error) {
console.log('Error received:', error.message);
return;
}
console.log('Result:', result);
});
Example: Global error handlers
// Browser: Global error handler
window.onerror = function(message, source, lineno, colno, error) {
console.log('Global error caught:');
console.log('Message:', message);
console.log('Source:', source);
console.log('Line:', lineno, 'Column:', colno);
console.log('Error object:', error);
// Return true to prevent default error handling
return true;
};
// Browser: Unhandled promise rejections
window.onunhandledrejection = function(event) {
console.log('Unhandled rejection:');
console.log('Reason:', event.reason);
console.log('Promise:', event.promise);
// Prevent default handling
event.preventDefault();
};
// Alternative with addEventListener
window.addEventListener('error', (event) => {
console.log('Error event:', event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled rejection:', event.reason);
});
// Node.js: Global error handlers
process.on('uncaughtException', (error, origin) => {
console.error('Uncaught exception:');
console.error('Error:', error);
console.error('Origin:', origin);
// Clean up and exit
// process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:');
console.error('Reason:', reason);
console.error('Promise:', promise);
});
// Example: Triggering global handlers
setTimeout(() => {
throw new Error('Uncaught error in setTimeout');
}, 100);
Promise.reject(new Error('Unhandled promise rejection'));
// Centralized error logger
class ErrorLogger {
static log(error, context = {}) {
const errorInfo = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
context,
url: typeof window !== 'undefined' ? window.location.href : null,
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null
};
console.error('Error logged:', errorInfo);
// Send to monitoring service
// sendToMonitoring(errorInfo);
}
}
window.onerror = function(message, source, lineno, colno, error) {
ErrorLogger.log(error || new Error(message), {
source,
lineno,
colno
});
return true;
};
window.onunhandledrejection = function(event) {
ErrorLogger.log(
event.reason instanceof Error
? event.reason
: new Error(String(event.reason)),
{type: 'unhandled-rejection'}
);
};
Example: Error recovery strategies
// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
await retryWithBackoff(async () => {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
return response.json();
});
// Fallback pattern
async function fetchWithFallback(primaryUrl, fallbackUrl) {
try {
return await fetch(primaryUrl);
} catch (error) {
console.log('Primary failed, using fallback');
return await fetch(fallbackUrl);
}
}
// Circuit breaker
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
console.log(`Circuit breaker OPEN for ${this.timeout}ms`);
}
}
}
const breaker = new CircuitBreaker(3, 30000);
async function callExternalService() {
return await breaker.execute(async () => {
const response = await fetch('/external-api');
if (!response.ok) throw new Error('Service unavailable');
return response.json();
});
}
// Graceful degradation
async function getUserData(userId) {
try {
// Try to fetch from API
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (error) {
console.warn('API unavailable, using cached data');
try {
// Fallback to cache
return await getCachedUserData(userId);
} catch (cacheError) {
console.warn('Cache unavailable, using default data');
// Final fallback to defaults
return {
id: userId,
name: 'Guest User',
avatar: '/default-avatar.png'
};
}
}
}
Key Points: Exceptions propagate up synchronous call stack until
caught. Promises propagate rejections through chain. Event handlers and timers don't propagate errors. Use
window.onerror and onunhandledrejection for global handling. Implement retry with
exponential backoff for transient failures. Circuit breaker pattern prevents cascading failures.
19.4 Console API and Logging Methods
Console Logging Methods
| Method | Purpose | Output Style |
|---|---|---|
| console.log() | General logging | Standard output |
| console.info() | Informational messages | Info icon (browser dependent) |
| console.warn() | Warning messages | Yellow warning icon |
| console.error() | Error messages | Red error icon with stack trace |
| console.debug() | Debug messages (may be hidden) | Debug level (filterable) |
Console Formatting and Inspection
| Method | Syntax | Description |
|---|---|---|
| console.dir() | console.dir(object) |
Display object properties interactively |
| console.dirxml() | console.dirxml(node) |
Display XML/HTML element tree |
| console.table() | console.table(array) |
Display array/object as table |
| console.group() | console.group(label) |
Start collapsible group |
| console.groupCollapsed() | console.groupCollapsed(label) |
Start collapsed group |
| console.groupEnd() | console.groupEnd() |
End current group |
Console Utilities
| Method | Syntax | Description |
|---|---|---|
| console.assert() | console.assert(condition, message) |
Log error if condition false |
| console.count() | console.count(label) |
Count calls with label |
| console.countReset() | console.countReset(label) |
Reset counter for label |
| console.time() | console.time(label) |
Start timer with label |
| console.timeLog() | console.timeLog(label) |
Log current timer value |
| console.timeEnd() | console.timeEnd(label) |
Stop timer and log duration |
| console.trace() | console.trace(message) |
Log stack trace |
| console.clear() | console.clear() |
Clear console |
String Substitution Patterns
| Pattern | Type | Example |
|---|---|---|
| %s | String | console.log('Hello %s', 'World') |
| %d or %i | Integer | console.log('Count: %d', 42) |
| %f | Float | console.log('Pi: %f', 3.14159) |
| %o | Object | console.log('Object: %o', obj) |
| %O | Object (optimized) | console.log('Object: %O', obj) |
| %c | CSS styling | console.log('%cStyled', 'color: red') |
Example: Basic console methods
// Log levels
console.log('Standard log message');
console.info('Informational message');
console.warn('Warning message');
console.error('Error message');
console.debug('Debug message');
// Multiple arguments
console.log('Name:', 'John', 'Age:', 30, 'Active:', true);
// String substitution
console.log('Hello %s, you are %d years old', 'Alice', 25);
console.log('Pi is approximately %f', Math.PI);
// CSS styling
console.log(
'%cStyled Text',
'color: blue; font-size: 20px; font-weight: bold'
);
console.log(
'%cError: %cSomething went wrong',
'color: red; font-weight: bold',
'color: orange'
);
// Object inspection
const user = {
name: 'John',
age: 30,
address: {
city: 'New York',
country: 'USA'
}
};
console.log(user); // Standard object display
console.dir(user); // Interactive object properties
console.dir(user, {depth: null}); // Show all nested levels
// Table display
const users = [
{id: 1, name: 'Alice', age: 25},
{id: 2, name: 'Bob', age: 30},
{id: 3, name: 'Charlie', age: 35}
];
console.table(users);
console.table(users, ['name', 'age']); // Show specific columns
// DOM element
const element = document.getElementById('app');
console.log(element); // DOM representation
console.dir(element); // Object properties
console.dirxml(element); // XML tree structure
Example: Console grouping and organization
// Simple grouping
console.group('User Details');
console.log('Name: John');
console.log('Age: 30');
console.log('Email: john@example.com');
console.groupEnd();
// Nested groups
console.group('Application Startup');
console.log('Initializing...');
console.group('Loading Modules');
console.log('Module A loaded');
console.log('Module B loaded');
console.log('Module C loaded');
console.groupEnd();
console.group('Connecting to Services');
console.log('Database connected');
console.log('API connected');
console.groupEnd();
console.log('Startup complete');
console.groupEnd();
// Collapsed groups
console.groupCollapsed('Debug Information');
console.log('This is collapsed by default');
console.log('User must expand to see');
console.groupEnd();
// Organizing API calls
async function fetchUserData(userId) {
console.group(`Fetching user ${userId}`);
console.time('fetch-duration');
try {
console.log('Sending request...');
const response = await fetch(`/api/users/${userId}`);
console.log('Response:', response.status);
const data = await response.json();
console.log('Data received:', data);
console.timeEnd('fetch-duration');
console.groupEnd();
return data;
} catch (error) {
console.error('Fetch failed:', error);
console.timeEnd('fetch-duration');
console.groupEnd();
throw error;
}
}
// Function call tracing
function traceFunction(fn, name) {
return function(...args) {
console.group(`${name} called`);
console.log('Arguments:', args);
try {
const result = fn(...args);
console.log('Result:', result);
console.groupEnd();
return result;
} catch (error) {
console.error('Error:', error);
console.groupEnd();
throw error;
}
};
}
const add = traceFunction((a, b) => a + b, 'add');
add(2, 3); // Logs group with args and result
Example: Console utilities
// Assertions
console.assert(1 === 1, 'This will not log');
console.assert(1 === 2, 'This WILL log as error');
function divide(a, b) {
console.assert(b !== 0, 'Division by zero');
return a / b;
}
divide(10, 0); // Logs assertion error
// Counting
function processItem(item) {
console.count('processItem');
console.log('Processing:', item);
}
processItem('A'); // "processItem: 1"
processItem('B'); // "processItem: 2"
processItem('C'); // "processItem: 3"
console.countReset('processItem');
processItem('D'); // "processItem: 1" (reset)
// Counting with labels
function handleEvent(type) {
console.count(type);
}
handleEvent('click'); // "click: 1"
handleEvent('scroll'); // "scroll: 1"
handleEvent('click'); // "click: 2"
handleEvent('scroll'); // "scroll: 2"
// Timing operations
console.time('operation');
// Simulate work
for (let i = 0; i < 1000000; i++) {
// work
}
console.timeLog('operation'); // Intermediate timing
// More work
console.timeEnd('operation'); // Final timing and stop
// Timing async operations
async function measureAsync() {
console.time('async-operation');
await fetch('/api/data');
console.timeLog('async-operation', 'After fetch');
await processData();
console.timeLog('async-operation', 'After processing');
console.timeEnd('async-operation');
}
// Multiple timers
console.time('timer-1');
console.time('timer-2');
setTimeout(() => console.timeEnd('timer-1'), 100);
setTimeout(() => console.timeEnd('timer-2'), 200);
// Stack trace
function a() {
b();
}
function b() {
c();
}
function c() {
console.trace('Called from c()');
}
a(); // Shows full call stack: c <- b <- a
// Performance monitoring
function measurePerformance(fn, name) {
console.time(name);
console.count(name);
try {
const result = fn();
console.timeEnd(name);
return result;
} catch (error) {
console.error(`${name} failed:`, error);
console.timeEnd(name);
throw error;
}
}
measurePerformance(() => {
// Some operation
return Array(1000).fill(0).map((_, i) => i * 2);
}, 'array-operation');
Example: Custom logger implementation
// Production-ready logger
class Logger {
constructor(options = {}) {
this.level = options.level || 'info';
this.prefix = options.prefix || '';
this.levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
}
shouldLog(level) {
return this.levels[level] >= this.levels[this.level];
}
format(level, ...args) {
const timestamp = new Date().toISOString();
const prefix = this.prefix ? `[${this.prefix}]` : '';
return [`[${timestamp}]`, prefix, `[${level.toUpperCase()}]`, ...args];
}
debug(...args) {
if (this.shouldLog('debug')) {
console.debug(...this.format('debug', ...args));
}
}
info(...args) {
if (this.shouldLog('info')) {
console.info(...this.format('info', ...args));
}
}
warn(...args) {
if (this.shouldLog('warn')) {
console.warn(...this.format('warn', ...args));
}
}
error(...args) {
if (this.shouldLog('error')) {
console.error(...this.format('error', ...args));
console.trace();
}
}
group(label) {
console.group(...this.format('group', label));
}
groupEnd() {
console.groupEnd();
}
time(label) {
console.time(label);
}
timeEnd(label) {
console.timeEnd(label);
}
}
// Usage
const logger = new Logger({
level: 'info',
prefix: 'MyApp'
});
logger.debug('This will not show (below info level)');
logger.info('Application started');
logger.warn('Low memory');
logger.error('Fatal error occurred');
// Module-specific loggers
const apiLogger = new Logger({level: 'debug', prefix: 'API'});
const dbLogger = new Logger({level: 'info', prefix: 'DB'});
apiLogger.debug('Request received');
dbLogger.info('Query executed');
// Environment-aware logger
const isDevelopment = process.env.NODE_ENV === 'development';
const appLogger = new Logger({
level: isDevelopment ? 'debug' : 'warn',
prefix: 'App'
});
appLogger.debug('This shows only in development');
Key Points: Use
console.log() for general, console.warn() for
warnings,
console.error() for errors. console.table() displays arrays beautifully. Group related
logs with console.group(). console.time() measures performance. Use
console.assert() for condition checks. %c enables CSS styling.
Implement custom logger for production with log levels.
19.5 Debugging Techniques and Breakpoint Usage
Breakpoint Types
| Type | Usage | When to Use |
|---|---|---|
| Line breakpoint | Click line number in debugger | Pause at specific line |
| Conditional breakpoint | Right-click line, add condition | Pause only when condition true |
| Logpoint | Log without stopping execution | Non-intrusive logging |
| debugger statement | debugger; in code |
Programmatic breakpoint |
| DOM breakpoint | Break on attribute/subtree changes | Debug DOM mutations |
| XHR breakpoint | Break on network requests | Debug AJAX calls |
| Event listener breakpoint | Break on event types | Debug event handling |
| Exception breakpoint | Break on caught/uncaught exceptions | Debug error handling |
Debugger Commands
| Command | Shortcut | Action |
|---|---|---|
| Continue | F8 / Cmd+\ | Resume execution until next breakpoint |
| Step Over | F10 / Cmd+' | Execute current line, don't enter functions |
| Step Into | F11 / Cmd+; | Enter function calls |
| Step Out | Shift+F11 / Shift+Cmd+; | Exit current function |
| Step | F9 | Single-step through code (Node.js) |
Debug Console Commands
| Command | Description | Example |
|---|---|---|
| Evaluate expression | Type any JavaScript expression | user.name, 2 + 2 |
| Modify variables | Change values while paused | count = 10 |
| Call functions | Execute functions | processData() |
| $0 - $4 | Recently inspected elements | $0.classList.add('active') |
| $_ | Last evaluated expression | $_ * 2 |
| $() | querySelector shorthand | $('#myId') |
| $$() | querySelectorAll shorthand | $$('.myClass') |
Debugging Best Practices
| Practice | Description | Benefit |
|---|---|---|
| Source maps | Map minified code to original | Debug production builds |
| Conditional breakpoints | Break only on specific conditions | Avoid breaking on every iteration |
| Watch expressions | Monitor variable values | Track state changes |
| Call stack inspection | View function call hierarchy | Understand execution flow |
| Blackbox scripts | Skip library code | Focus on application code |
Example: Debugger statement and breakpoints
// debugger statement
function calculateTotal(items) {
let total = 0;
for (const item of items) {
debugger; // Execution pauses here when DevTools open
total += item.price * item.quantity;
}
return total;
}
// Conditional debugger
function processUsers(users) {
for (const user of users) {
// Only break for specific user
if (user.id === 123) {
debugger;
}
processUser(user);
}
}
// Debug with assertions
function divide(a, b) {
console.assert(typeof a === 'number', 'a must be number');
console.assert(typeof b === 'number', 'b must be number');
console.assert(b !== 0, 'Division by zero');
return a / b;
}
// Debug wrapper
function debug(fn, name) {
return function(...args) {
console.group(`Debug: ${name}`);
console.log('Arguments:', args);
console.time(name);
debugger; // Pause before execution
try {
const result = fn(...args);
console.log('Result:', result);
console.timeEnd(name);
console.groupEnd();
return result;
} catch (error) {
console.error('Error:', error);
console.timeEnd(name);
console.groupEnd();
throw error;
}
};
}
const debugAdd = debug((a, b) => a + b, 'add');
debugAdd(2, 3);
// Breakpoint on error
function riskyOperation() {
try {
// risky code
throw new Error('Something went wrong');
} catch (error) {
debugger; // Pause to inspect error
console.error('Error caught:', error);
}
}
Example: Debugging async code
// Debug promise chain
fetch('/api/users')
.then(response => {
debugger; // Inspect response
return response.json();
})
.then(data => {
debugger; // Inspect parsed data
console.log('Users:', data);
})
.catch(error => {
debugger; // Inspect error
console.error('Fetch failed:', error);
});
// Debug async/await
async function fetchUserData(userId) {
try {
console.log('Fetching user:', userId);
debugger; // Before fetch
const response = await fetch(`/api/users/${userId}`);
debugger; // After fetch, inspect response
const data = await response.json();
debugger; // After parsing, inspect data
return data;
} catch (error) {
debugger; // On error
console.error('Error:', error);
throw error;
}
}
// Debug with detailed logging
async function debugAsyncOperation(operation, name) {
console.group(`Async: ${name}`);
console.time(name);
try {
console.log('Starting...');
const result = await operation();
console.log('Completed:', result);
console.timeEnd(name);
console.groupEnd();
return result;
} catch (error) {
console.error('Failed:', error);
console.trace();
console.timeEnd(name);
console.groupEnd();
throw error;
}
}
// Usage
await debugAsyncOperation(
() => fetch('/api/data').then(r => r.json()),
'fetch-data'
);
// Debug race conditions
let requestCount = 0;
async function debugRaceCondition() {
const id = ++requestCount;
console.log(`Request ${id} started`);
debugger;
await new Promise(r => setTimeout(r, Math.random() * 1000));
console.log(`Request ${id} completed`);
debugger;
}
// Multiple concurrent calls
Promise.all([
debugRaceCondition(),
debugRaceCondition(),
debugRaceCondition()
]);
Example: Advanced debugging techniques
// Performance profiling
function profileFunction(fn, iterations = 1000) {
console.time('profile');
for (let i = 0; i < iterations; i++) {
fn();
}
console.timeEnd('profile');
}
profileFunction(() => {
// Code to profile
const arr = Array(100).fill(0).map((_, i) => i * 2);
});
// Memory usage tracking (Chrome)
function checkMemory() {
if (performance.memory) {
console.group('Memory Usage');
console.log('Used:',
Math.round(performance.memory.usedJSHeapSize / 1048576), 'MB');
console.log('Total:',
Math.round(performance.memory.totalJSHeapSize / 1048576), 'MB');
console.log('Limit:',
Math.round(performance.memory.jsHeapSizeLimit / 1048576), 'MB');
console.groupEnd();
}
}
// Before operation
checkMemory();
// Do memory-intensive work
const largeArray = Array(1000000).fill({data: 'test'});
// After operation
checkMemory();
// Stack trace analysis
function captureCallers() {
const stack = new Error().stack;
const lines = stack.split('\n');
console.group('Call Stack');
lines.slice(2).forEach(line => { // Skip Error and this function
console.log(line.trim());
});
console.groupEnd();
}
function a() { b(); }
function b() { c(); }
function c() { captureCallers(); }
a();
// Debugging higher-order functions
function debugHOF(fn, name) {
return function(...args) {
console.group(`HOF: ${name}`);
console.log('Input:', args);
const result = fn(...args);
console.log('Output:', result);
console.groupEnd();
return result;
};
}
const numbers = [1, 2, 3, 4, 5];
const debugMap = debugHOF(x => x * 2, 'double');
const debugFilter = debugHOF(x => x > 2, 'greaterThan2');
const debugReduce = debugHOF((a, b) => a + b, 'sum');
const doubled = numbers.map(debugMap);
const filtered = numbers.filter(debugFilter);
const sum = numbers.reduce(debugReduce, 0);
// Debug event listeners
function debugEventListener(element, event, handler) {
element.addEventListener(event, function(...args) {
console.group(`Event: ${event}`);
console.log('Target:', args[0].target);
console.log('Event:', args[0]);
console.trace('Call stack');
const result = handler.apply(this, args);
console.groupEnd();
return result;
});
}
const button = document.getElementById('myButton');
debugEventListener(button, 'click', (e) => {
console.log('Button clicked');
});
Key Points: Use
debugger statement for programmatic breakpoints. Conditional
breakpoints prevent breaking on every iteration. Step Over skips function calls,
Step Into enters them. Inspect call stack to understand execution flow. Use
watch expressions to monitor variables. Enable source maps for debugging transpiled code.
19.6 Error Monitoring and Production Debugging
Error Monitoring Services
| Service | Features | Use Case |
|---|---|---|
| Sentry | Error tracking, performance, releases | Full-featured error monitoring |
| Rollbar | Real-time error tracking | Error aggregation and alerts |
| Bugsnag | Error monitoring, stability score | Mobile and web apps |
| LogRocket | Session replay, error tracking | User session debugging |
| Datadog | APM, logging, error tracking | Full observability platform |
| New Relic | APM, browser monitoring | Application performance |
Error Context Data
| Category | Data | Purpose |
|---|---|---|
| User Info | ID, email, username | Identify affected users |
| Environment | Browser, OS, device | Reproduce environment |
| Application | Version, build, release | Track regressions |
| Request | URL, method, headers | Understand context |
| Breadcrumbs | User actions, navigation | Trace user journey |
| Custom Tags | Feature flags, experiments | Segment and filter errors |
Production Debugging Strategies
| Strategy | Implementation | Benefit |
|---|---|---|
| Source maps | Upload to error monitoring service | Debug minified code |
| Structured logging | JSON format with context | Searchable, parseable logs |
| Correlation IDs | Track requests across services | Distributed tracing |
| Feature flags | Enable debug mode for specific users | Production debugging |
| Error sampling | Rate limit error reports | Reduce noise, cost |
| Release tracking | Tag errors with version | Identify regressions |
Example: Error monitoring setup
// Sentry integration example
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: process.env.NODE_ENV,
release: 'my-app@1.0.0',
// Performance monitoring
tracesSampleRate: 0.1,
// Error filtering
beforeSend(event, hint) {
// Filter out certain errors
if (event.exception) {
const error = hint.originalException;
// Ignore network errors
if (error.message.includes('NetworkError')) {
return null;
}
// Ignore specific status codes
if (error.statusCode === 404) {
return null;
}
}
return event;
},
// PII scrubbing
beforeBreadcrumb(breadcrumb) {
if (breadcrumb.category === 'xhr') {
// Remove sensitive data from URLs
breadcrumb.data.url = breadcrumb.data.url.replace(
/token=([^&]+)/,
'token=[REDACTED]'
);
}
return breadcrumb;
}
});
// Set user context
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username
});
// Set custom tags
Sentry.setTag('feature_flag_new_ui', 'enabled');
Sentry.setTag('subscription_tier', 'premium');
// Set custom context
Sentry.setContext('app', {
build: '12345',
commit: 'abc123'
});
// Manual error reporting
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error, {
level: 'error',
tags: {
operation: 'riskyOperation'
},
contexts: {
operation: {
input: operationInput,
attemptNumber: retryCount
}
}
});
}
// Breadcrumbs for debugging
Sentry.addBreadcrumb({
category: 'auth',
message: 'User logged in',
level: 'info'
});
Sentry.addBreadcrumb({
category: 'navigation',
message: 'Navigated to /dashboard',
level: 'info'
});
// Capture message (non-error)
Sentry.captureMessage('Unusual condition detected', 'warning');
Example: Production logging system
// Structured logger for production
class ProductionLogger {
constructor(config = {}) {
this.serviceName = config.serviceName || 'app';
this.environment = config.environment || 'production';
this.version = config.version || '1.0.0';
}
createLogEntry(level, message, context = {}) {
return {
timestamp: new Date().toISOString(),
level,
message,
service: this.serviceName,
environment: this.environment,
version: this.version,
correlation_id: context.correlationId || this.generateCorrelationId(),
user_id: context.userId,
session_id: context.sessionId,
url: typeof window !== 'undefined' ? window.location.href : undefined,
user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
...context.extra
};
}
generateCorrelationId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
log(level, message, context) {
const entry = this.createLogEntry(level, message, context);
// Send to logging service
this.send(entry);
// Also log to console in development
if (this.environment === 'development') {
console[level](message, entry);
}
}
async send(entry) {
try {
await fetch('/api/logs', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(entry)
});
} catch (error) {
// Fallback: log to console
console.error('Failed to send log:', error);
}
}
info(message, context) {
this.log('info', message, context);
}
warn(message, context) {
this.log('warn', message, context);
}
error(message, error, context = {}) {
this.log('error', message, {
...context,
extra: {
...context.extra,
error_name: error.name,
error_message: error.message,
error_stack: error.stack
}
});
}
}
const logger = new ProductionLogger({
serviceName: 'frontend-app',
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION
});
// Usage
logger.info('User logged in', {
userId: '123',
extra: {
login_method: 'oauth'
}
});
try {
await fetchData();
} catch (error) {
logger.error('Failed to fetch data', error, {
userId: currentUser.id,
extra: {
endpoint: '/api/data',
retry_count: 3
}
});
}
Example: Error sampling and rate limiting
// Error sampling to reduce noise
class ErrorSampler {
constructor(sampleRate = 0.1) {
this.sampleRate = sampleRate;
this.errorCounts = new Map();
this.resetInterval = 60000; // 1 minute
setInterval(() => this.errorCounts.clear(), this.resetInterval);
}
shouldReport(error) {
const errorKey = `${error.name}:${error.message}`;
const count = this.errorCounts.get(errorKey) || 0;
// Always report first occurrence
if (count === 0) {
this.errorCounts.set(errorKey, 1);
return true;
}
// Sample subsequent occurrences
this.errorCounts.set(errorKey, count + 1);
return Math.random() < this.sampleRate;
}
report(error, context = {}) {
if (this.shouldReport(error)) {
// Send to monitoring service
errorMonitoring.captureException(error, {
...context,
extra: {
...context.extra,
error_count: this.errorCounts.get(
`${error.name}:${error.message}`
)
}
});
}
}
}
const sampler = new ErrorSampler(0.1); // 10% sample rate
// Use in error handler
window.onerror = function(message, source, lineno, colno, error) {
sampler.report(error || new Error(message), {
extra: {source, lineno, colno}
});
};
// Rate limiter for specific error types
class ErrorRateLimiter {
constructor(maxErrors = 10, windowMs = 60000) {
this.maxErrors = maxErrors;
this.windowMs = windowMs;
this.errors = new Map();
}
canReport(errorType) {
const now = Date.now();
const errors = this.errors.get(errorType) || [];
// Remove old errors outside window
const recentErrors = errors.filter(
time => now - time < this.windowMs
);
if (recentErrors.length >= this.maxErrors) {
return false;
}
recentErrors.push(now);
this.errors.set(errorType, recentErrors);
return true;
}
}
const rateLimiter = new ErrorRateLimiter(10, 60000);
function reportError(error) {
if (rateLimiter.canReport(error.name)) {
errorMonitoring.captureException(error);
} else {
console.warn(`Rate limit exceeded for ${error.name}`);
}
}
Example: Release tracking and version management
// Release tracking
class ReleaseTracker {
constructor(config) {
this.version = config.version;
this.buildId = config.buildId;
this.gitCommit = config.gitCommit;
this.environment = config.environment;
this.initMonitoring();
}
initMonitoring() {
// Configure error monitoring with release info
errorMonitoring.setRelease({
version: this.version,
buildId: this.buildId,
commit: this.gitCommit
});
// Track release deployment
this.trackDeployment();
}
async trackDeployment() {
try {
await fetch('/api/deployments', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
version: this.version,
buildId: this.buildId,
commit: this.gitCommit,
environment: this.environment,
timestamp: new Date().toISOString()
})
});
} catch (error) {
console.error('Failed to track deployment:', error);
}
}
async compareWithPrevious() {
// Fetch error rates from monitoring service
const currentErrors = await this.getErrorRate(this.version);
const previousErrors = await this.getErrorRate(this.previousVersion);
const increase = currentErrors - previousErrors;
const percentIncrease = (increase / previousErrors) * 100;
if (percentIncrease > 50) {
this.alertHighErrorRate(percentIncrease);
}
}
}
// Initialize on app start
const tracker = new ReleaseTracker({
version: process.env.APP_VERSION,
buildId: process.env.BUILD_ID,
gitCommit: process.env.GIT_COMMIT,
environment: process.env.NODE_ENV
});
// Feature flag debugging
class FeatureFlagDebugger {
constructor() {
this.flags = new Map();
this.debugEnabled = false;
}
enableDebug(userId) {
// Enable verbose logging for specific user
if (this.isDebugUser(userId)) {
this.debugEnabled = true;
console.log('Debug mode enabled for user:', userId);
}
}
isDebugUser(userId) {
// Check if user is in debug list
return ['debug-user-1', 'admin'].includes(userId);
}
log(...args) {
if (this.debugEnabled) {
console.log('[DEBUG]', ...args);
}
}
setFlag(name, value) {
this.flags.set(name, value);
this.log(`Feature flag set: ${name} = ${value}`);
}
getFlag(name) {
const value = this.flags.get(name);
this.log(`Feature flag get: ${name} = ${value}`);
return value;
}
}
const featureFlags = new FeatureFlagDebugger();
// Enable debug mode for specific user
if (currentUser.id === 'debug-user-1') {
featureFlags.enableDebug(currentUser.id);
}
Best Practices: Use error monitoring service like Sentry or
Rollbar.
Include context: user, environment, version. Implement error sampling to reduce
noise. Upload source maps for debugging minified code. Track releases to identify regressions. Use structured
logging with correlation IDs. Implement rate limiting for error reporting. Enable debug mode for specific users
in production.
Section 19 Summary: Error Handling and Debugging Techniques
- Error Types: Error, TypeError, ReferenceError, RangeError, SyntaxError, URIError, AggregateError
- Error Properties: name, message, stack, cause (ES2022)
- Custom Errors: Extend Error class, add domain-specific properties
- Exception Propagation: Sync code up call stack, promises through chain, async/await
- Global Handlers: window.onerror, onunhandledrejection, process uncaughtException
- Error Recovery: Retry with backoff, fallback, circuit breaker, graceful degradation
- Console API: log, warn, error, table, group, time, trace, assert
- Debugging: debugger statement, breakpoints, step commands, watch expressions
- Production Monitoring: Sentry, Rollbar, structured logging, source maps
- Best Practices: Error sampling, rate limiting, release tracking, correlation IDs
20. Memory Management and Performance
20.1 Garbage Collection and Memory Lifecycle
JavaScript Memory Lifecycle
| Phase | Description | Automatic/Manual |
|---|---|---|
| 1. Allocation | Memory allocated for variables, objects | Automatic |
| 2. Usage | Read/write operations on allocated memory | Manual (developer code) |
| 3. Deallocation | Release unused memory back to system | Automatic (GC) |
Garbage Collection Algorithms
| Algorithm | Strategy | Usage |
|---|---|---|
| Reference Counting (legacy) | Track number of references to object | Old browsers (circular ref issues) |
| Mark-and-Sweep | Mark reachable objects, sweep unreachable | Modern browsers (default) |
| Generational GC | Separate young/old objects | V8 (Chrome, Node.js) |
| Incremental GC | Split GC work into small chunks | Reduce pause times |
| Concurrent GC | Run GC in parallel with app | V8 Orinoco |
V8 Heap Structure
| Space | Purpose | GC Strategy |
|---|---|---|
| New Space (Young Gen) | Recently allocated objects | Scavenge (fast, frequent) |
| Old Space (Old Gen) | Objects survived multiple GC cycles | Mark-Sweep-Compact (slower, less frequent) |
| Large Object Space | Objects > 1MB | Never moved |
| Code Space | Compiled code | Mark-Sweep |
| Map Space | Hidden classes (object shapes) | Mark-Sweep |
Reachability and GC Roots
| GC Root | Description | Example |
|---|---|---|
| Global object | window (browser), global (Node.js) | window.myGlobal |
| Local variables | Variables in current execution context | Function parameters, local vars |
| Closures | Variables captured by closures | Outer scope variables |
| Active stack | Call stack variables | Variables in executing functions |
| DOM elements | Elements attached to document | document.body references |
Example: Understanding garbage collection
// Objects become eligible for GC when unreachable
function createObjects() {
const obj1 = {data: new Array(1000).fill('x')};
const obj2 = {data: new Array(1000).fill('y')};
// Both objects are reachable (local variables)
console.log('Objects created');
return obj1; // obj2 becomes unreachable after return
}
const result = createObjects();
// obj2 is now eligible for GC
// result (obj1) is still reachable
// Make obj1 eligible for GC
result = null; // or let it go out of scope
// Circular references (handled by mark-and-sweep)
function createCircularRef() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2; // obj1 references obj2
obj2.ref = obj1; // obj2 references obj1
// Both have references to each other
// But when function returns, both become unreachable
// Mark-and-sweep handles this correctly
}
createCircularRef(); // Both objects will be GC'd
// Closure keeping object alive
function createClosure() {
const largeData = new Array(1000000).fill('data');
return function() {
// This closure keeps largeData alive
console.log(largeData.length);
};
}
const fn = createClosure();
// largeData is kept in memory because closure references it
// Release closure and its captured variables
fn = null; // Now largeData can be GC'd
// Global variables never collected
window.globalData = new Array(1000000); // Stays until page unload
// WeakMap/WeakSet allow GC
const cache = new WeakMap();
function cacheUserData(user) {
// user object is key, data is value
cache.set(user, {expensive: 'computation'});
}
let user = {id: 1, name: 'Alice'};
cacheUserData(user);
// When user is no longer referenced elsewhere
user = null;
// The WeakMap entry will be GC'd automatically
// Regular Map prevents GC
const regularCache = new Map();
let user2 = {id: 2, name: 'Bob'};
regularCache.set(user2, {data: 'value'});
user2 = null;
// Object is still in Map, won't be GC'd
// Must manually: regularCache.delete(user2)
Example: Monitoring memory usage
// Check memory usage (Chrome only)
if (performance.memory) {
console.log('Memory usage:');
console.log('Used JS Heap:',
Math.round(performance.memory.usedJSHeapSize / 1048576), 'MB');
console.log('Total JS Heap:',
Math.round(performance.memory.totalJSHeapSize / 1048576), 'MB');
console.log('JS Heap Limit:',
Math.round(performance.memory.jsHeapSizeLimit / 1048576), 'MB');
}
// Memory monitor
class MemoryMonitor {
constructor(interval = 5000) {
this.interval = interval;
this.measurements = [];
}
start() {
this.timer = setInterval(() => {
if (performance.memory) {
const measurement = {
timestamp: Date.now(),
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit
};
this.measurements.push(measurement);
this.checkForLeaks();
}
}, this.interval);
}
stop() {
clearInterval(this.timer);
}
checkForLeaks() {
if (this.measurements.length < 10) return;
// Check if memory is consistently growing
const recent = this.measurements.slice(-10);
const growthRate = recent.map((m, i) => {
if (i === 0) return 0;
return m.used - recent[i - 1].used;
});
const avgGrowth = growthRate.reduce((a, b) => a + b, 0) / growthRate.length;
if (avgGrowth > 1048576) { // 1MB average growth
console.warn('Potential memory leak detected');
console.log('Average growth:', Math.round(avgGrowth / 1024), 'KB/measurement');
}
}
getReport() {
if (this.measurements.length === 0) return null;
const first = this.measurements[0];
const last = this.measurements[this.measurements.length - 1];
return {
duration: last.timestamp - first.timestamp,
initialMemory: Math.round(first.used / 1048576),
currentMemory: Math.round(last.used / 1048576),
growth: Math.round((last.used - first.used) / 1048576),
measurements: this.measurements.length
};
}
}
const monitor = new MemoryMonitor(5000);
monitor.start();
// Later...
const report = monitor.getReport();
console.log('Memory Report:', report);
monitor.stop();
// Force garbage collection (Node.js with --expose-gc flag)
if (global.gc) {
console.log('Before GC:', process.memoryUsage().heapUsed);
global.gc();
console.log('After GC:', process.memoryUsage().heapUsed);
}
// Node.js memory usage
function logMemoryUsage() {
const usage = process.memoryUsage();
console.log('Memory Usage:');
console.log('RSS:', Math.round(usage.rss / 1048576), 'MB');
console.log('Heap Total:', Math.round(usage.heapTotal / 1048576), 'MB');
console.log('Heap Used:', Math.round(usage.heapUsed / 1048576), 'MB');
console.log('External:', Math.round(usage.external / 1048576), 'MB');
}
Key Points: JavaScript uses automatic garbage collection.
Modern engines use mark-and-sweep algorithm. V8 uses generational GC (young/old).
Objects are GC'd when unreachable from GC roots. Circular references are handled
correctly. Use WeakMap/WeakSet for cache that allows GC. Monitor memory with
performance.memory.
20.2 Memory Leak Prevention and Detection
Common Memory Leak Patterns
| Pattern | Cause | Solution |
|---|---|---|
| Global variables | Accidental globals, never released | Use strict mode, const/let, proper scoping |
| Forgotten timers | setInterval/setTimeout not cleared | clearInterval, clearTimeout in cleanup |
| Event listeners | Listeners not removed | removeEventListener in cleanup |
| Closures | Large data captured in closure | Limit scope, nullify references |
| Detached DOM | DOM elements removed but referenced | Remove JS references to deleted elements |
| Cache without limit | Unbounded cache growth | Use WeakMap, LRU cache, size limits |
| Console.log references | Objects logged kept in memory | Remove logs in production |
Detection Tools and Techniques
| Tool | Purpose | How to Use |
|---|---|---|
| Chrome DevTools Memory | Heap snapshots, allocation timeline | Performance > Memory, take snapshots |
| Heap Snapshot Comparison | Find objects not being freed | Take 2+ snapshots, compare |
| Allocation Timeline | Track allocations over time | Record timeline, analyze patterns |
| performance.memory | Monitor heap size programmatically | Check usedJSHeapSize periodically |
| Node.js --inspect | Debug Node.js memory | node --inspect app.js |
| process.memoryUsage() | Node.js memory stats | Log heapUsed, rss |
Prevention Best Practices
| Practice | Description | Benefit |
|---|---|---|
| Use strict mode | Prevent accidental globals | Catches undeclared variables |
| Cleanup in lifecycle | Remove listeners, clear timers | Proper resource management |
| WeakMap/WeakSet | Allow garbage collection | Automatic cache cleanup |
| Limit closure scope | Don't capture unnecessary data | Reduce memory footprint |
| Nullify references | Set to null when done | Make objects eligible for GC |
| Use object pools | Reuse objects instead of creating | Reduce allocation pressure |
Example: Common memory leak patterns
// LEAK 1: Accidental global
function createLeak1() {
// Missing 'const', creates global
leakedVariable = 'This is global'; // BAD
}
createLeak1();
console.log(window.leakedVariable); // Accessible globally
// FIX: Use strict mode and proper declarations
'use strict';
function noLeak1() {
const properVariable = 'This is local'; // GOOD
}
// LEAK 2: Forgotten timer
function createLeak2() {
const largeData = new Array(1000000).fill('data');
setInterval(() => {
console.log(largeData.length); // Keeps largeData in memory
}, 1000); // BAD: Never cleared
}
createLeak2();
// FIX: Clear timer
function noLeak2() {
const largeData = new Array(1000000).fill('data');
const timer = setInterval(() => {
console.log(largeData.length);
}, 1000);
// Clear when done
setTimeout(() => clearInterval(timer), 10000); // GOOD
}
// LEAK 3: Event listener not removed
class ComponentWithLeak {
constructor() {
this.data = new Array(1000000).fill('data');
// BAD: Listener keeps component in memory
document.addEventListener('click', this.handleClick.bind(this));
}
handleClick() {
console.log(this.data.length);
}
// No cleanup method
}
// FIX: Remove listener
class ComponentWithoutLeak {
constructor() {
this.data = new Array(1000000).fill('data');
this.boundHandleClick = this.handleClick.bind(this);
document.addEventListener('click', this.boundHandleClick);
}
handleClick() {
console.log(this.data.length);
}
destroy() {
// GOOD: Remove listener
document.removeEventListener('click', this.boundHandleClick);
this.data = null;
}
}
// LEAK 4: Detached DOM nodes
let detachedNodes = [];
function createLeak4() {
const div = document.createElement('div');
div.innerHTML = '<p>Content</p>';
document.body.appendChild(div);
// Store reference
detachedNodes.push(div);
// Remove from DOM but still referenced
document.body.removeChild(div); // BAD: Still in detachedNodes array
}
// FIX: Remove all references
function noLeak4() {
const div = document.createElement('div');
div.innerHTML = '<p>Content</p>';
document.body.appendChild(div);
// When removing from DOM, clear references
document.body.removeChild(div);
div = null; // GOOD
}
// LEAK 5: Closures capturing large data
function createLeak5() {
const hugeArray = new Array(1000000).fill('data');
return function() {
// This closure captures hugeArray even if not used
console.log('Function called');
};
}
const fn = createLeak5(); // hugeArray kept in memory
// FIX: Limit closure scope
function noLeak5() {
const hugeArray = new Array(1000000).fill('data');
const length = hugeArray.length; // Extract only what's needed
return function() {
console.log('Array had', length, 'elements'); // GOOD
// hugeArray is not captured
};
}
// LEAK 6: Unbounded cache
const cache = {};
function addToCache(key, value) {
cache[key] = value; // BAD: Grows forever
}
// FIX: Use WeakMap or size limit
const properCache = new WeakMap();
// Or LRU cache with size limit
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
set(key, value) {
// Remove oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
get(key) {
const value = this.cache.get(key);
if (value !== undefined) {
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
}
Example: Memory leak detection
// Automated leak detection
class LeakDetector {
constructor(threshold = 50 * 1024 * 1024) { // 50MB
this.threshold = threshold;
this.measurements = [];
this.warnings = 0;
}
measure() {
if (!performance.memory) {
console.warn('performance.memory not available');
return;
}
const current = performance.memory.usedJSHeapSize;
this.measurements.push({
timestamp: Date.now(),
heapSize: current
});
// Keep last 100 measurements
if (this.measurements.length > 100) {
this.measurements.shift();
}
this.analyze();
}
analyze() {
if (this.measurements.length < 10) return;
const first = this.measurements[0];
const last = this.measurements[this.measurements.length - 1];
const growth = last.heapSize - first.heapSize;
const duration = last.timestamp - first.timestamp;
const growthRate = growth / (duration / 1000); // bytes per second
// Check if consistently growing
if (growth > this.threshold) {
this.warnings++;
console.warn('Memory leak suspected!');
console.log('Growth:', Math.round(growth / 1048576), 'MB');
console.log('Duration:', Math.round(duration / 1000), 'seconds');
console.log('Rate:', Math.round(growthRate / 1024), 'KB/s');
this.captureSnapshot();
}
}
captureSnapshot() {
console.log('Capturing heap snapshot...');
// In real implementation, take heap snapshot via DevTools Protocol
}
startMonitoring(interval = 5000) {
this.timer = setInterval(() => this.measure(), interval);
}
stopMonitoring() {
clearInterval(this.timer);
}
}
const detector = new LeakDetector();
detector.startMonitoring(5000);
// Leak test helper
async function testForLeaks(fn, iterations = 10) {
if (!performance.memory) {
console.log('Memory profiling not available');
return;
}
// Force GC if available
if (global.gc) global.gc();
const before = performance.memory.usedJSHeapSize;
for (let i = 0; i < iterations; i++) {
fn();
}
// Force GC if available
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 1000));
const after = performance.memory.usedJSHeapSize;
const growth = after - before;
console.log('Memory before:', Math.round(before / 1024), 'KB');
console.log('Memory after:', Math.round(after / 1024), 'KB');
console.log('Growth:', Math.round(growth / 1024), 'KB');
if (growth > 1024 * 1024) { // 1MB
console.warn('Possible memory leak detected');
} else {
console.log('No significant memory growth');
}
}
// Test a function for leaks
await testForLeaks(() => {
// Function to test
const data = new Array(10000).fill('test');
// Should be cleaned up after each iteration
});
Example: Proper cleanup patterns
// React-style lifecycle with cleanup
class Component {
constructor() {
this.timers = [];
this.listeners = [];
this.subscriptions = [];
}
mount() {
// Add timer
const timer = setInterval(() => {
this.update();
}, 1000);
this.timers.push(timer);
// Add event listener
const handler = this.handleClick.bind(this);
document.addEventListener('click', handler);
this.listeners.push({element: document, event: 'click', handler});
// Add subscription
const sub = eventBus.subscribe('data', this.handleData.bind(this));
this.subscriptions.push(sub);
}
unmount() {
// Clean up timers
this.timers.forEach(timer => clearInterval(timer));
this.timers = [];
// Remove event listeners
this.listeners.forEach(({element, event, handler}) => {
element.removeEventListener(event, handler);
});
this.listeners = [];
// Unsubscribe
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
// Nullify large data
this.data = null;
}
handleClick(e) {
console.log('Clicked');
}
handleData(data) {
this.data = data;
}
update() {
console.log('Update');
}
}
// Cleanup helper
class CleanupManager {
constructor() {
this.cleanupFns = [];
}
register(cleanupFn) {
this.cleanupFns.push(cleanupFn);
}
cleanup() {
this.cleanupFns.forEach(fn => {
try {
fn();
} catch (error) {
console.error('Cleanup error:', error);
}
});
this.cleanupFns = [];
}
}
// Usage
const manager = new CleanupManager();
// Register cleanup for timer
const timer = setInterval(() => {}, 1000);
manager.register(() => clearInterval(timer));
// Register cleanup for listener
const handler = () => {};
document.addEventListener('click', handler);
manager.register(() => document.removeEventListener('click', handler));
// Later: cleanup all
manager.cleanup();
// Automatic cleanup with Symbol.dispose (TC39 proposal)
class ResourceWithCleanup {
constructor() {
this.resource = acquireResource();
}
[Symbol.dispose]() {
this.resource.close();
this.resource = null;
}
}
// Will auto-cleanup when leaving scope
{
using resource = new ResourceWithCleanup();
// Use resource
} // Automatic cleanup here
Key Points: Common leaks: globals, forgotten timers, event listeners, closures, detached DOM.
Use Chrome DevTools Memory profiler for detection. Take heap snapshots and
compare. Always clean up:
removeEventListener, clearInterval. Use
WeakMap/WeakSet for auto-cleanup caches. Implement proper lifecycle cleanup.
Test for leaks by running operations repeatedly.
20.3 Performance Optimization Strategies
Rendering Performance
| Strategy | Technique | Impact |
|---|---|---|
| Minimize DOM access | Cache DOM references, batch reads/writes | Reduce layout thrashing |
| Avoid layout thrashing | Separate read and write operations | Prevent forced reflows |
| Use DocumentFragment | Build DOM off-document, append once | Single reflow instead of many |
| Virtual scrolling | Render only visible items | Handle large lists efficiently |
| Debounce/throttle | Limit expensive operations frequency | Reduce CPU usage |
| requestAnimationFrame | Sync visual changes with refresh rate | Smooth animations, no jank |
JavaScript Performance
| Strategy | Technique | Impact |
|---|---|---|
| Avoid premature optimization | Profile first, optimize bottlenecks | Focus effort where it matters |
| Use efficient algorithms | Choose O(n log n) over O(n²) | Better scalability |
| Minimize object creation | Reuse objects, object pools | Reduce GC pressure |
| Memoization | Cache expensive computation results | Avoid redundant calculations |
| Lazy evaluation | Defer work until needed | Faster initial load |
| Web Workers | Move heavy computation off main thread | Keep UI responsive |
Network Performance
| Strategy | Technique | Impact |
|---|---|---|
| Minimize bundle size | Code splitting, tree shaking | Faster download and parse |
| Compress assets | Gzip, Brotli compression | Smaller transfer size |
| Cache effectively | HTTP caching, service workers | Avoid re-downloading |
| CDN usage | Serve assets from edge locations | Reduced latency |
| Resource hints | preload, prefetch, dns-prefetch | Anticipate needed resources |
| HTTP/2 | Multiplexing, server push | Better resource loading |
Example: Avoiding layout thrashing
// BAD: Layout thrashing (alternating read/write)
function layoutThrashing() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
// Read (causes layout)
const height = el.offsetHeight;
// Write (invalidates layout)
el.style.height = height + 10 + 'px';
// Next read will force reflow
});
}
// GOOD: Batch reads, then writes
function noLayoutThrashing() {
const elements = document.querySelectorAll('.item');
// Phase 1: Read all
const heights = Array.from(elements).map(el => el.offsetHeight);
// Phase 2: Write all
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});
}
// Use FastDOM library for automatic batching
fastdom.measure(() => {
const height = element.offsetHeight;
fastdom.mutate(() => {
element.style.height = height + 10 + 'px';
});
});
// Efficient DOM manipulation
// BAD: Multiple reflows
function inefficientDOMUpdate() {
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // Reflow on each append
}
}
// GOOD: Single reflow with DocumentFragment
function efficientDOMUpdate() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div); // No reflow
}
document.body.appendChild(fragment); // Single reflow
}
// GOOD: Clone and replace
function efficientDOMReplace() {
const container = document.getElementById('container');
const clone = container.cloneNode(false); // Empty clone
// Build new content
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
clone.appendChild(div);
}
// Replace in one operation
container.parentNode.replaceChild(clone, container);
}
Example: Debounce and throttle
// Debounce: Wait for quiet period
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Search as user types (wait for pause)
const searchInput = document.getElementById('search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
// Expensive API call
fetch(`/api/search?q=${query}`);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttle: Limit execution frequency
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage: Scroll event (execute at most once per 100ms)
const throttledScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Update UI based on scroll
}, 100);
window.addEventListener('scroll', throttledScroll);
// requestAnimationFrame throttle (better for visuals)
function rafThrottle(func) {
let rafId = null;
return function(...args) {
if (rafId === null) {
rafId = requestAnimationFrame(() => {
func.apply(this, args);
rafId = null;
});
}
};
}
const rafThrottledScroll = rafThrottle(() => {
// Smooth visual updates
updateScrollIndicator();
});
window.addEventListener('scroll', rafThrottledScroll);
// Debounce with immediate option
function debounceImmediate(func, delay) {
let timeoutId;
return function(...args) {
const callNow = !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// Execute immediately, then debounce
const immediateDebounce = debounceImmediate((e) => {
console.log('Button clicked');
}, 1000);
button.addEventListener('click', immediateDebounce);
Example: Memoization and caching
// Simple memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Expensive computation
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.time('first');
console.log(memoizedFib(40)); // Slow first time
console.timeEnd('first');
console.time('second');
console.log(memoizedFib(40)); // Instant from cache
console.timeEnd('second');
// Memoization with TTL
function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, {
value: result,
timestamp: Date.now()
});
return result;
};
}
const cachedFetch = memoizeWithTTL(async (url) => {
const response = await fetch(url);
return response.json();
}, 5000); // 5 second cache
// LRU Cache for bounded memory
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
// Move to end (most recent)
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
// Delete if exists (to reinsert at end)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// Evict oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
// Memoize with LRU
function memoizeLRU(fn, maxSize = 100) {
const cache = new LRUCache(maxSize);
return function(...args) {
const key = JSON.stringify(args);
let result = cache.get(key);
if (result === undefined) {
result = fn.apply(this, args);
cache.set(key, result);
}
return result;
};
}
// Object pool for reusing objects
class ObjectPool {
constructor(factory, reset, initialSize = 10) {
this.factory = factory;
this.reset = reset;
this.available = [];
for (let i = 0; i < initialSize; i++) {
this.available.push(factory());
}
}
acquire() {
return this.available.length > 0
? this.available.pop()
: this.factory();
}
release(obj) {
this.reset(obj);
this.available.push(obj);
}
}
// Usage
const vectorPool = new ObjectPool(
() => ({x: 0, y: 0, z: 0}),
(v) => {
v.x = 0;
v.y = 0;
v.z = 0;
}
);
function processVectors() {
const v1 = vectorPool.acquire();
const v2 = vectorPool.acquire();
// Use vectors
v1.x = 10;
v2.y = 20;
// Release back to pool
vectorPool.release(v1);
vectorPool.release(v2);
}
Key Points: Avoid layout thrashing by batching DOM reads/writes.
Use
debounce for search, throttle for scroll/resize.
requestAnimationFrame
for smooth animations. Memoize expensive computations. Use LRU cache for bounded
memory. Object pools reduce GC pressure. Profile before optimizing - measure, don't guess.
20.4 Profiling and Performance Monitoring
Performance API Methods
| Method | Purpose | Returns |
|---|---|---|
| performance.now() | High-resolution timestamp | DOMHighResTimeStamp (microsecond precision) |
| performance.mark() | Create named timestamp | undefined |
| performance.measure() | Measure between two marks | Performance entry |
| performance.getEntries() | Get all performance entries | Array of PerformanceEntry |
| performance.getEntriesByType() | Filter entries by type | Array of PerformanceEntry |
| performance.getEntriesByName() | Get entries by name | Array of PerformanceEntry |
| performance.clearMarks() | Clear marks | undefined |
| performance.clearMeasures() | Clear measures | undefined |
Performance Entry Types
| Type | Description | Key Properties |
|---|---|---|
| navigation | Page navigation timing | domContentLoadedEventEnd, loadEventEnd |
| resource | Resource loading (scripts, images) | duration, transferSize, initiatorType |
| mark | Custom timestamp markers | startTime, name |
| measure | Time between marks | duration, startTime |
| paint | Paint timing (FCP, FP) | startTime |
| longtask | Tasks > 50ms (Long Task API) | duration, startTime |
Core Web Vitals
| Metric | Measures | Good Threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading performance | < 2.5 seconds |
| FID (First Input Delay) | Interactivity | < 100 milliseconds |
| CLS (Cumulative Layout Shift) | Visual stability | < 0.1 |
| FCP (First Contentful Paint) | Perceived load speed | < 1.8 seconds |
| TTFB (Time to First Byte) | Server response time | < 600 milliseconds |
| TTI (Time to Interactive) | When page becomes interactive | < 3.8 seconds |
Chrome DevTools Profiling
| Tool | Purpose | Usage |
|---|---|---|
| Performance tab | Record runtime performance | Identify bottlenecks, long tasks |
| Memory tab | Heap snapshots, allocation timeline | Find memory leaks |
| Lighthouse | Automated performance audit | Get recommendations |
| Coverage tab | Find unused JavaScript/CSS | Reduce bundle size |
| Network tab | Monitor requests and timing | Optimize loading |
Example: Performance measurement
// Basic timing with performance.now()
const start = performance.now();
// Some operation
for (let i = 0; i < 1000000; i++) {
// work
}
const end = performance.now();
console.log(`Operation took ${end - start} milliseconds`);
// User Timing API with marks and measures
performance.mark('fetch-start');
fetch('/api/data')
.then(response => response.json())
.then(data => {
performance.mark('fetch-end');
// Measure between marks
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
const measure = performance.getEntriesByName('fetch-duration')[0];
console.log(`Fetch took ${measure.duration}ms`);
});
// Measure component render
function measureRender(component, props) {
performance.mark(`${component}-render-start`);
const result = renderComponent(component, props);
performance.mark(`${component}-render-end`);
performance.measure(
`${component}-render`,
`${component}-render-start`,
`${component}-render-end`
);
return result;
}
// Get all measures
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
});
// Clear marks and measures
performance.clearMarks();
performance.clearMeasures();
// Navigation timing
const navTiming = performance.getEntriesByType('navigation')[0];
console.log('Page Load Metrics:');
console.log('DNS lookup:', navTiming.domainLookupEnd - navTiming.domainLookupStart, 'ms');
console.log('TCP connection:', navTiming.connectEnd - navTiming.connectStart, 'ms');
console.log('Request time:', navTiming.responseStart - navTiming.requestStart, 'ms');
console.log('Response time:', navTiming.responseEnd - navTiming.responseStart, 'ms');
console.log('DOM processing:', navTiming.domComplete - navTiming.domLoading, 'ms');
console.log('Load event:', navTiming.loadEventEnd - navTiming.loadEventStart, 'ms');
// Resource timing
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log(resource.name);
console.log(' Duration:', resource.duration.toFixed(2), 'ms');
console.log(' Transfer size:', resource.transferSize, 'bytes');
console.log(' Type:', resource.initiatorType);
});
// Find slow resources
const slowResources = resources.filter(r => r.duration > 1000);
console.log('Resources > 1s:', slowResources.length);
Example: Performance monitoring class
// Comprehensive performance monitor
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.observers = [];
this.initObservers();
}
initObservers() {
// Observe long tasks (> 50ms)
if ('PerformanceLongTaskTiming' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', entry.duration, 'ms');
this.recordMetric('long-task', entry.duration);
}
});
observer.observe({entryTypes: ['longtask']});
this.observers.push(observer);
}
// Observe layout shifts (CLS)
if ('LayoutShift' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
this.recordMetric('layout-shift', entry.value);
}
}
});
observer.observe({entryTypes: ['layout-shift']});
this.observers.push(observer);
}
// Observe largest contentful paint (LCP)
if ('LargestContentfulPaint' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric('lcp', lastEntry.startTime);
console.log('LCP:', lastEntry.startTime, 'ms');
});
observer.observe({entryTypes: ['largest-contentful-paint']});
this.observers.push(observer);
}
// Observe first input delay (FID)
if ('PerformanceEventTiming' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const fid = entry.processingStart - entry.startTime;
this.recordMetric('fid', fid);
console.log('FID:', fid, 'ms');
}
});
observer.observe({entryTypes: ['first-input']});
this.observers.push(observer);
}
}
recordMetric(name, value) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push({
value,
timestamp: Date.now()
});
}
measureFunction(name, fn) {
const start = performance.now();
try {
const result = fn();
const duration = performance.now() - start;
this.recordMetric(name, duration);
if (duration > 50) {
console.warn(`${name} took ${duration.toFixed(2)}ms`);
}
return result;
} catch (error) {
const duration = performance.now() - start;
this.recordMetric(name, duration);
throw error;
}
}
async measureAsync(name, fn) {
const start = performance.now();
try {
const result = await fn();
const duration = performance.now() - start;
this.recordMetric(name, duration);
return result;
} catch (error) {
const duration = performance.now() - start;
this.recordMetric(name, duration);
throw error;
}
}
getReport() {
const report = {};
for (const [name, values] of this.metrics) {
const durations = values.map(v => v.value);
report[name] = {
count: durations.length,
min: Math.min(...durations),
max: Math.max(...durations),
avg: durations.reduce((a, b) => a + b, 0) / durations.length,
p50: this.percentile(durations, 0.5),
p95: this.percentile(durations, 0.95),
p99: this.percentile(durations, 0.99)
};
}
return report;
}
percentile(values, p) {
const sorted = values.slice().sort((a, b) => a - b);
const index = Math.ceil(sorted.length * p) - 1;
return sorted[index];
}
sendToAnalytics() {
const report = this.getReport();
// Send to analytics service
navigator.sendBeacon('/api/performance', JSON.stringify(report));
}
cleanup() {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
}
// Usage
const monitor = new PerformanceMonitor();
// Measure function
monitor.measureFunction('processData', () => {
// Some work
return processData();
});
// Measure async function
await monitor.measureAsync('fetchData', async () => {
return await fetch('/api/data').then(r => r.json());
});
// Get performance report
const report = monitor.getReport();
console.log('Performance Report:', report);
// Send to analytics on page unload
window.addEventListener('beforeunload', () => {
monitor.sendToAnalytics();
});
Example: Core Web Vitals measurement
// Measure Core Web Vitals
class WebVitals {
constructor() {
this.vitals = {};
}
measureLCP() {
if (!('LargestContentfulPaint' in window)) return;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.vitals.lcp = lastEntry.startTime;
// Report
this.reportVital('LCP', lastEntry.startTime);
});
observer.observe({entryTypes: ['largest-contentful-paint']});
}
measureFID() {
if (!('PerformanceEventTiming' in window)) return;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const fid = entry.processingStart - entry.startTime;
this.vitals.fid = fid;
this.reportVital('FID', fid);
}
});
observer.observe({entryTypes: ['first-input']});
}
measureCLS() {
if (!('LayoutShift' in window)) return;
let clsScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.vitals.cls = clsScore;
});
observer.observe({entryTypes: ['layout-shift']});
// Report on page hide
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.reportVital('CLS', clsScore);
}
});
}
measureFCP() {
if (!('PerformancePaintTiming' in window)) return;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.vitals.fcp = entry.startTime;
this.reportVital('FCP', entry.startTime);
}
}
});
observer.observe({entryTypes: ['paint']});
}
measureTTFB() {
const navTiming = performance.getEntriesByType('navigation')[0];
if (navTiming) {
const ttfb = navTiming.responseStart - navTiming.requestStart;
this.vitals.ttfb = ttfb;
this.reportVital('TTFB', ttfb);
}
}
reportVital(name, value) {
console.log(`${name}:`, value.toFixed(2), 'ms');
// Check against thresholds
const thresholds = {
LCP: {good: 2500, needsImprovement: 4000},
FID: {good: 100, needsImprovement: 300},
CLS: {good: 0.1, needsImprovement: 0.25},
FCP: {good: 1800, needsImprovement: 3000},
TTFB: {good: 600, needsImprovement: 1500}
};
const threshold = thresholds[name];
if (threshold) {
let rating;
if (value <= threshold.good) {
rating = 'good';
} else if (value <= threshold.needsImprovement) {
rating = 'needs improvement';
} else {
rating = 'poor';
}
console.log(`${name} rating: ${rating}`);
}
// Send to analytics
this.sendToAnalytics(name, value);
}
sendToAnalytics(name, value) {
// Send to analytics service
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', JSON.stringify({
metric: name,
value: value,
timestamp: Date.now(),
url: window.location.href
}));
}
}
measureAll() {
this.measureLCP();
this.measureFID();
this.measureCLS();
this.measureFCP();
this.measureTTFB();
}
}
// Initialize
const webVitals = new WebVitals();
webVitals.measureAll();
// Alternative: Use web-vitals library
import {getLCP, getFID, getCLS, getFCP, getTTFB} from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
getFCP(console.log);
getTTFB(console.log);
Key Points: Use
performance.now() for precise timing.
performance.mark()
and performance.measure() for custom metrics. Monitor Core Web
Vitals:
LCP, FID, CLS. Use PerformanceObserver to track long tasks, layout shifts. Send metrics to
analytics
with navigator.sendBeacon(). Profile with Chrome DevTools Performance tab.
20.5 Code Splitting and Lazy Loading
Code Splitting Strategies
| Strategy | When to Use | Implementation |
|---|---|---|
| Route-based | Different pages/routes | Split by route, load on navigation |
| Component-based | Large/heavy components | Dynamic import, lazy load |
| Vendor splitting | Third-party libraries | Separate vendor bundle, cache long-term |
| Feature-based | Optional features | Load only when feature used |
| Conditional | Environment-specific code | Load based on conditions |
Dynamic Import Syntax
| Syntax | Returns | Usage |
|---|---|---|
| import() | Promise<Module> | Dynamic module loading |
| await import() | Module object | Top-level await or async function |
| import().then() | Promise chain | Handle module in .then() |
Lazy Loading Techniques
| Technique | Target | Trigger |
|---|---|---|
| Intersection Observer | Images, components | When entering viewport |
| On interaction | Modal, tooltip, menu | Click, hover, focus |
| On route change | Page components | Navigation event |
| Prefetching | Likely-needed code | Idle time, link hover |
| Loading attribute | Images, iframes | <img loading="lazy"> |
Bundle Optimization
| Technique | Purpose | Tool |
|---|---|---|
| Tree shaking | Remove unused exports | Webpack, Rollup |
| Minification | Reduce code size | Terser, UglifyJS |
| Compression | Reduce transfer size | Gzip, Brotli |
| Scope hoisting | Flatten module scope | Webpack ModuleConcatenationPlugin |
| Bundle analysis | Visualize bundle size | webpack-bundle-analyzer |
Example: Dynamic imports
// Basic dynamic import
button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.doSomething();
});
// Import specific exports
const {default: Component} = await import('./Component.js');
const {helper1, helper2} = await import('./helpers.js');
// Conditional import
async function loadAnalytics() {
if (user.hasConsent) {
const {init} = await import('./analytics.js');
init();
}
}
// Route-based code splitting
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();
const Page = module.default;
renderPage(Page);
}
}
// Component lazy loading with React
import React, {lazy, Suspense} from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// Lazy load on interaction
class LazyButton {
constructor(element) {
this.element = element;
this.loaded = false;
// Load on first click
element.addEventListener('click', this.onClick.bind(this), {once: true});
}
async onClick(event) {
if (!this.loaded) {
const {handler} = await import('./buttonHandler.js');
this.loaded = true;
// Call the handler
handler(event);
// Future clicks go directly to handler
this.element.addEventListener('click', handler);
}
}
}
// Prefetch on hover
link.addEventListener('mouseenter', async () => {
// Prefetch when user hovers (likely to click)
await import('./nextPage.js');
}, {once: true});
// Webpack magic comments for chunking
const LazyComponent = lazy(() =>
import(
/* webpackChunkName: "heavy-component" */
/* webpackPrefetch: true */
'./HeavyComponent'
)
);
Example: Intersection Observer for lazy loading
// Lazy load images
class LazyImageLoader {
constructor(options = {}) {
this.rootMargin = options.rootMargin || '50px';
this.threshold = options.threshold || 0.01;
this.observer = new IntersectionObserver(
this.onIntersection.bind(this),
{
rootMargin: this.rootMargin,
threshold: this.threshold
}
);
}
observe(element) {
this.observer.observe(element);
}
onIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
if (src) {
img.src = src;
}
if (srcset) {
img.srcset = srcset;
}
img.classList.add('loaded');
}
}
// Usage
const loader = new LazyImageLoader();
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => loader.observe(img));
// HTML:
// <img data-src="image.jpg" alt="Lazy loaded image">
// Lazy load components
class LazyComponentLoader {
constructor() {
this.observer = new IntersectionObserver(
this.onIntersection.bind(this),
{rootMargin: '100px'}
);
this.components = new Map();
}
register(element, loader) {
this.components.set(element, loader);
this.observer.observe(element);
}
async onIntersection(entries) {
for (const entry of entries) {
if (entry.isIntersecting) {
const element = entry.target;
const loader = this.components.get(element);
if (loader) {
await this.loadComponent(element, loader);
this.observer.unobserve(element);
this.components.delete(element);
}
}
}
}
async loadComponent(element, loader) {
try {
element.classList.add('loading');
const module = await loader();
const Component = module.default;
// Render component
const instance = new Component(element);
instance.render();
element.classList.remove('loading');
element.classList.add('loaded');
} catch (error) {
console.error('Failed to load component:', error);
element.classList.add('error');
}
}
}
// Usage
const componentLoader = new LazyComponentLoader();
const heavySection = document.getElementById('heavy-section');
componentLoader.register(
heavySection,
() => import('./HeavySection.js')
);
// Native lazy loading for images
// <img src="image.jpg" loading="lazy" alt="...">
// Polyfill for browsers without native support
if ('loading' in HTMLImageElement.prototype) {
// Native lazy loading supported
const images = document.querySelectorAll('img[loading="lazy"]');
// Nothing to do
} else {
// Fallback to Intersection Observer
const images = document.querySelectorAll('img[loading="lazy"]');
const loader = new LazyImageLoader();
images.forEach(img => {
img.dataset.src = img.src;
img.removeAttribute('src');
loader.observe(img);
});
}
Example: Module loading strategies
// Module loader with caching
class ModuleLoader {
constructor() {
this.cache = new Map();
this.loading = new Map();
}
async load(path) {
// Return cached module
if (this.cache.has(path)) {
return this.cache.get(path);
}
// Return existing promise if already loading
if (this.loading.has(path)) {
return this.loading.get(path);
}
// Start loading
const promise = import(path)
.then(module => {
this.cache.set(path, module);
this.loading.delete(path);
return module;
})
.catch(error => {
this.loading.delete(path);
throw error;
});
this.loading.set(path, promise);
return promise;
}
preload(paths) {
paths.forEach(path => {
if (!this.cache.has(path) && !this.loading.has(path)) {
this.load(path).catch(() => {
// Ignore preload errors
});
}
});
}
clear(path) {
this.cache.delete(path);
this.loading.delete(path);
}
}
const moduleLoader = new ModuleLoader();
// Load module
const module = await moduleLoader.load('./module.js');
// Preload modules
moduleLoader.preload([
'./page1.js',
'./page2.js',
'./page3.js'
]);
// Priority-based loading
class PriorityLoader {
constructor() {
this.queues = {
high: [],
medium: [],
low: []
};
this.processing = false;
}
load(path, priority = 'medium') {
return new Promise((resolve, reject) => {
this.queues[priority].push({
path,
resolve,
reject
});
this.process();
});
}
async process() {
if (this.processing) return;
this.processing = true;
while (this.hasWork()) {
const item = this.getNext();
if (item) {
try {
const module = await import(item.path);
item.resolve(module);
} catch (error) {
item.reject(error);
}
}
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
this.processing = false;
}
hasWork() {
return this.queues.high.length > 0 ||
this.queues.medium.length > 0 ||
this.queues.low.length > 0;
}
getNext() {
if (this.queues.high.length > 0) {
return this.queues.high.shift();
}
if (this.queues.medium.length > 0) {
return this.queues.medium.shift();
}
if (this.queues.low.length > 0) {
return this.queues.low.shift();
}
return null;
}
}
const priorityLoader = new PriorityLoader();
// Load critical module with high priority
await priorityLoader.load('./critical.js', 'high');
// Load nice-to-have module with low priority
priorityLoader.load('./analytics.js', 'low');
Key Points: Use
import() for dynamic code
splitting.
Split by route, component, or feature. Use IntersectionObserver for lazy loading images/components.
Prefetch likely-needed modules on hover/idle. Native loading="lazy" for images. Tree shaking
removes
unused code. Analyze bundles with webpack-bundle-analyzer. Cache loaded modules to avoid re-loading.
20.6 Optimization for V8 Engine
V8 Optimization Concepts
| Concept | Description | Impact |
|---|---|---|
| Hidden Classes | Object shape/structure | Property access performance |
| Inline Caching | Cache property lookup locations | Faster repeated access |
| JIT Compilation | Compile hot code to machine code | Execution speed |
| Optimization Tiers | Ignition (interpreter), TurboFan (optimizer) | Progressive optimization |
| Deoptimization | Fall back to interpreter on type change | Performance penalty |
Optimization-Friendly Patterns
| Pattern | Why It's Fast | Example |
|---|---|---|
| Monomorphic functions | Same types always, optimizer-friendly | Always pass same object shape |
| Fixed object structure | Stable hidden class | Initialize all properties in constructor |
| Array of same type | Optimized element access | All numbers or all objects |
| Small functions | Can be inlined | Single responsibility |
| Avoid delete | Maintains hidden class | Set to null/undefined instead |
| Predictable types | Reduces deoptimization | Consistent return types |
Common Deoptimization Triggers
| Trigger | Why It's Slow | Solution |
|---|---|---|
| Dynamic property addition | Changes hidden class | Initialize all properties upfront |
| delete operator | Breaks hidden class optimization | Set to null/undefined |
| Polymorphic functions | Multiple code paths | Use monomorphic versions |
| Sparse arrays | Dictionary mode storage | Use dense arrays |
| Mixed array types | Generic element storage | Homogeneous arrays |
| try-catch in hot path | Prevents optimization | Isolate try-catch to helper function |
Example: Hidden classes and object shape
// BAD: Dynamic property addition (different hidden classes)
function createPoint1() {
const point = {};
point.x = 10; // Hidden class transition 1
point.y = 20; // Hidden class transition 2
return point;
}
const p1 = createPoint1();
const p2 = createPoint1();
p2.z = 30; // p2 now has different hidden class than p1
// GOOD: Initialize all properties (same hidden class)
function createPoint2() {
return {
x: 10, // All properties at once
y: 20,
z: 0
};
}
const p3 = createPoint2();
const p4 = createPoint2();
// p3 and p4 share the same hidden class
// GOOD: Constructor with fixed structure
class Point {
constructor(x, y) {
this.x = x; // Always same order
this.y = y; // Always same properties
}
}
// BAD: Inconsistent object structure
function createUser(name, age, email) {
const user = {name, age};
// Sometimes add email, sometimes don't
if (email) {
user.email = email; // Different hidden class!
}
return user;
}
// GOOD: Always same structure
function createUser2(name, age, email = null) {
return {
name,
age,
email // Always present, even if null
};
}
// BAD: Delete property (kills optimization)
const obj = {a: 1, b: 2, c: 3};
delete obj.b; // BAD: Changes hidden class
// GOOD: Set to undefined
const obj2 = {a: 1, b: 2, c: 3};
obj2.b = undefined; // GOOD: Maintains hidden class
// Monomorphic vs Polymorphic functions
// BAD: Polymorphic (accepts different shapes)
function processData(data) {
if (Array.isArray(data)) {
return data.map(x => x * 2);
} else if (typeof data === 'object') {
return Object.values(data).map(x => x * 2);
}
return data * 2;
}
// GOOD: Monomorphic (always same type)
function processArray(arr) {
return arr.map(x => x * 2);
}
function processObject(obj) {
return Object.values(obj).map(x => x * 2);
}
function processNumber(num) {
return num * 2;
}
Example: Array optimization
// BAD: Mixed types (generic array storage)
const mixed = [1, 'two', {three: 3}, null];
// V8 can't optimize - uses generic storage
// GOOD: Homogeneous types (optimized storage)
const numbers = [1, 2, 3, 4, 5];
const strings = ['a', 'b', 'c'];
const objects = [{id: 1}, {id: 2}, {id: 3}];
// BAD: Sparse array (dictionary mode)
const sparse = [];
sparse[0] = 'a';
sparse[1000] = 'b'; // Creates sparse array
// V8 switches to hash table storage
// GOOD: Dense array
const dense = new Array(1001).fill(null);
dense[0] = 'a';
dense[1000] = 'b';
// BAD: Holey array (slower access)
const holey = [1, 2, 3];
holey[10] = 11; // Creates holes at 3-9
// GOOD: Pre-size and fill
const filled = Array(11).fill(0);
filled[0] = 1;
filled[1] = 2;
filled[2] = 3;
filled[10] = 11;
// Array access patterns
// GOOD: Sequential access
function sumSequential(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
// GOOD: forEach (also optimized)
function sumForEach(arr) {
let sum = 0;
arr.forEach(x => {
sum += x;
});
return sum;
}
// Array element kinds (fastest to slowest)
// PACKED_SMI_ELEMENTS (small integers)
const smi = [1, 2, 3, 4, 5];
// PACKED_DOUBLE_ELEMENTS (doubles)
const doubles = [1.1, 2.2, 3.3, 4.4, 5.5];
// PACKED_ELEMENTS (objects)
const objects2 = [{}, {}, {}];
// HOLEY_SMI_ELEMENTS (holes + small integers)
const holeSmi = [1, 2, , 4, 5];
// DICTIONARY_ELEMENTS (slowest)
const dict = [];
dict[100000] = 1;
Example: Function optimization
// BAD: Try-catch in hot path
function hotFunction(x) {
try {
return x * 2;
} catch (e) {
return 0;
}
}
// GOOD: Isolate try-catch
function hotFunction2(x) {
return x * 2; // Can be optimized
}
function safeHotFunction(x) {
try {
return hotFunction2(x);
} catch (e) {
return 0;
}
}
// BAD: Large function (won't inline)
function largeFunction(a, b, c, d, e) {
// 100+ lines of code
// ...many operations
return result;
}
// GOOD: Small functions (can inline)
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function compute(a, b) {
return multiply(add(a, b), 2); // Inlined
}
// BAD: Type-changing return
function inconsistentReturn(flag) {
if (flag) {
return 42; // Number
}
return 'error'; // String - causes deopt
}
// GOOD: Consistent return type
function consistentReturn(flag) {
if (flag) {
return {success: true, value: 42};
}
return {success: false, error: 'Failed'};
}
// Function inlining budget
// Small functions (< 600 bytes bytecode) can be inlined
// BAD: Megamorphic call site (4+ different types)
function process(obj) {
return obj.getValue(); // If called with 4+ different shapes
}
// Call with many different object shapes
process(new ClassA());
process(new ClassB());
process(new ClassC());
process(new ClassD());
process(new ClassE()); // Goes megamorphic!
// GOOD: Monomorphic (1 type) or Polymorphic (2-3 types)
function processA(obj) {
return obj.getValue(); // Only ClassA
}
function processB(obj) {
return obj.getValue(); // Only ClassB
}
// Object allocation optimization
// GOOD: Allocate in function (can escape analysis)
function createLocal() {
const obj = {x: 1, y: 2};
return obj.x + obj.y; // obj doesn't escape
}
// Escape analysis can eliminate allocation
// GOOD: Predictable control flow
function predictable(x) {
if (x > 0) {
return x * 2;
}
return 0;
}
// BAD: Complex control flow
function unpredictable(x) {
switch (x % 7) {
case 0: return x * 1;
case 1: return x * 2;
case 2: return x * 3;
// ... many cases
}
}
Key Points: V8 uses hidden classes for object shape. Initialize
all properties at once. Avoid
delete, use = undefined. Keep arrays homogeneous (same
type).
Avoid sparse/holey arrays. Monomorphic functions (same types) optimize best. Small functions can be inlined.
Isolate try-catch from hot paths. Consistent return types prevent deoptimization.
Section 20 Summary: Memory Management and Performance
- Garbage Collection: Automatic mark-and-sweep, generational (young/old), V8 Orinoco
- Reachability: Objects GC'd when unreachable from roots (global, stack, closures)
- Memory Leaks: Globals, timers, listeners, closures, detached DOM, unbounded caches
- Prevention: Cleanup lifecycle, WeakMap/WeakSet, remove listeners, clear timers
- Detection: Chrome DevTools Memory, heap snapshots, performance.memory monitoring
- Performance: Minimize DOM access, batch reads/writes, debounce/throttle
- Profiling: performance.mark/measure, PerformanceObserver, Core Web Vitals (LCP, FID, CLS)
- Code Splitting: Dynamic import(), route/component-based, lazy loading with IntersectionObserver
- V8 Optimization: Hidden classes, monomorphic functions, homogeneous arrays, avoid delete
- Best Practices: Profile first, optimize bottlenecks, memoization, object pools, tree shaking
21. Web APIs and Browser Integration
21.1 Fetch API and HTTP Request Handling
Fetch API Basics
| Method | Syntax | Returns |
|---|---|---|
| fetch() | fetch(url, options?) | Promise<Response> |
| response.json() | response.json() | Promise<any> |
| response.text() | response.text() | Promise<string> |
| response.blob() | response.blob() | Promise<Blob> |
| response.arrayBuffer() | response.arrayBuffer() | Promise<ArrayBuffer> |
| response.formData() | response.formData() | Promise<FormData> |
Response Properties
| Property | Type | Description |
|---|---|---|
| response.ok | boolean | True if status 200-299 |
| response.status | number | HTTP status code (200, 404, etc.) |
| response.statusText | string | Status message ("OK", "Not Found") |
| response.headers | Headers | Response headers |
| response.url | string | Final URL (after redirects) |
| response.redirected | boolean | True if response redirected |
| response.type | string | "basic", "cors", "opaque", etc. |
Request Options
| Option | Type | Description |
|---|---|---|
| method | string | "GET", "POST", "PUT", "DELETE", etc. |
| headers | object | Request headers |
| body | string|FormData|Blob | Request body (not for GET/HEAD) |
| mode | string | "cors", "no-cors", "same-origin" |
| credentials | string | "omit", "same-origin", "include" |
| cache | string | "default", "no-cache", "reload", etc. |
| signal | AbortSignal | For request cancellation |
HTTP Status Code Categories
| Range | Category | Common Codes |
|---|---|---|
| 100-199 | Informational | 100 Continue, 101 Switching Protocols |
| 200-299 | Success | 200 OK, 201 Created, 204 No Content |
| 300-399 | Redirection | 301 Moved, 302 Found, 304 Not Modified |
| 400-499 | Client Error | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 500-599 | Server Error | 500 Internal Error, 503 Service Unavailable |
Example: Basic fetch requests
// GET request
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Fetch error:', error);
});
// Async/await version
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
// POST request with JSON
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Usage
await createUser({
name: 'John Doe',
email: 'john@example.com'
});
// PUT request
async function updateUser(id, updates) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
return response.json();
}
// DELETE request
async function deleteUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE'
});
if (response.status === 204) {
return {success: true};
}
return response.json();
}
// Upload file with FormData
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData // Don't set Content-Type header - browser sets it
});
return response.json();
}
// Request with timeout
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
Example: Advanced fetch patterns
// Retry logic
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Retry on server errors (5xx)
if (i === retries - 1) {
throw new Error(`Max retries reached. Status: ${response.status}`);
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
} catch (error) {
if (i === retries - 1) {
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
// Parallel requests
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.all(promises);
}
// Usage
const [users, posts, comments] = await fetchMultiple([
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments'
]);
// Sequential requests (when one depends on another)
async function fetchUserAndPosts(userId) {
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
return {user, posts};
}
// Progress tracking for upload
async function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Upload error')));
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// Usage
await uploadWithProgress(file, (percent) => {
console.log(`Upload progress: ${percent.toFixed(2)}%`);
});
// API client class
class ApiClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP error! status: ${response.status}`);
}
// Handle no-content responses
if (response.status === 204) {
return null;
}
return response.json();
}
get(endpoint, options) {
return this.request(endpoint, {...options, method: 'GET'});
}
post(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
put(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
delete(endpoint, options) {
return this.request(endpoint, {...options, method: 'DELETE'});
}
}
// Usage
const api = new ApiClient('https://api.example.com', {
'Authorization': 'Bearer token123'
});
const users = await api.get('/users');
const newUser = await api.post('/users', {name: 'John'});
await api.delete(`/users/${newUser.id}`);
Key Points:
fetch() returns Promise<Response>. Check
response.ok for success (status 200-299). Use response.json() to parse JSON.
AbortController for request cancellation. FormData for file uploads. Set headers for authentication
and content type. Handle errors with try-catch. Always check response.ok before
parsing body.
21.2 DOM Manipulation and Element Selection
Element Selection Methods
| Method | Returns | Description |
|---|---|---|
| document.getElementById() | Element | null | Select by ID |
| document.querySelector() | Element | null | First match of CSS selector |
| document.querySelectorAll() | NodeList | All matches of CSS selector |
| document.getElementsByClassName() | HTMLCollection (live) | Elements by class name |
| document.getElementsByTagName() | HTMLCollection (live) | Elements by tag name |
| element.closest() | Element | null | Nearest ancestor matching selector |
| element.matches() | boolean | Check if element matches selector |
Element Creation and Modification
| Method | Purpose | Usage |
|---|---|---|
| document.createElement() | Create element | createElement('div') |
| document.createTextNode() | Create text node | createTextNode('text') |
| element.cloneNode() | Clone element | cloneNode(true) for deep clone |
| element.appendChild() | Add child at end | parent.appendChild(child) |
| element.insertBefore() | Insert before reference | parent.insertBefore(new, ref) |
| element.removeChild() | Remove child | parent.removeChild(child) |
| element.remove() | Remove self | element.remove() |
| element.replaceChild() | Replace child | parent.replaceChild(new, old) |
Modern DOM Insertion Methods
| Method | Position | Accepts |
|---|---|---|
| element.append() | Inside, at end | Nodes or strings |
| element.prepend() | Inside, at start | Nodes or strings |
| element.before() | Before element (sibling) | Nodes or strings |
| element.after() | After element (sibling) | Nodes or strings |
| element.replaceWith() | Replace element | Nodes or strings |
Element Properties and Attributes
| Property/Method | Purpose | Example |
|---|---|---|
| element.innerHTML | Get/set HTML content | div.innerHTML = '<p>text</p>' |
| element.textContent | Get/set text (no HTML parsing) | div.textContent = 'text' |
| element.innerText | Get/set rendered text | div.innerText = 'text' |
| element.getAttribute() | Get attribute value | img.getAttribute('src') |
| element.setAttribute() | Set attribute | img.setAttribute('src', 'url') |
| element.removeAttribute() | Remove attribute | div.removeAttribute('class') |
| element.hasAttribute() | Check if attribute exists | div.hasAttribute('data-id') |
| element.dataset | Access data-* attributes | div.dataset.userId |
CSS Class Manipulation
| Method | Purpose | Example |
|---|---|---|
| element.classList.add() | Add class(es) | div.classList.add('active', 'highlighted') |
| element.classList.remove() | Remove class(es) | div.classList.remove('active') |
| element.classList.toggle() | Toggle class | div.classList.toggle('hidden') |
| element.classList.contains() | Check if has class | div.classList.contains('active') |
| element.classList.replace() | Replace class | div.classList.replace('old', 'new') |
| element.className | Get/set all classes (string) | div.className = 'class1 class2' |
Example: DOM selection and manipulation
// Element selection
const header = document.getElementById('header');
const firstButton = document.querySelector('.btn');
const allButtons = document.querySelectorAll('.btn');
const items = document.getElementsByClassName('item');
// Modern selection with optional chaining
const content = document.querySelector('.container')?.querySelector('.content');
// Creating elements
const div = document.createElement('div');
div.className = 'card';
div.id = 'user-card';
div.textContent = 'User Card';
// Setting attributes
div.setAttribute('data-user-id', '123');
div.setAttribute('role', 'article');
// Using dataset for data attributes
div.dataset.userId = '123'; // Creates data-user-id
div.dataset.userName = 'John'; // Creates data-user-name
console.log(div.dataset.userId); // '123'
// Creating complex structures
const card = document.createElement('div');
card.className = 'card';
const title = document.createElement('h3');
title.textContent = 'Card Title';
const content2 = document.createElement('p');
content2.textContent = 'Card content...';
const button = document.createElement('button');
button.textContent = 'Click Me';
button.className = 'btn btn-primary';
card.append(title, content2, button);
document.body.appendChild(card);
// Inserting elements
const container = document.querySelector('.container');
// Insert at end
container.append('Some text', div);
// Insert at start
container.prepend('First content');
// Insert before/after
const reference = document.querySelector('.reference');
reference.before(document.createElement('div'));
reference.after(document.createElement('div'));
// Replace element
const oldElement = document.querySelector('.old');
const newElement = document.createElement('div');
newElement.textContent = 'New element';
oldElement.replaceWith(newElement);
// Removing elements
const element = document.querySelector('.to-remove');
element.remove(); // Modern way
// Or
element.parentNode.removeChild(element); // Old way
// Clone elements
const original = document.querySelector('.original');
const clone = original.cloneNode(true); // Deep clone
document.body.appendChild(clone);
// Class manipulation
const box = document.querySelector('.box');
box.classList.add('active');
box.classList.remove('inactive');
box.classList.toggle('visible');
if (box.classList.contains('active')) {
console.log('Box is active');
}
// Toggle with force parameter
box.classList.toggle('active', true); // Always add
box.classList.toggle('active', false); // Always remove
// Replace class
box.classList.replace('old-class', 'new-class');
// Working with innerHTML vs textContent
const container2 = document.querySelector('.container');
// innerHTML - parses HTML
container2.innerHTML = '<strong>Bold text</strong>';
// textContent - treats as plain text (safer, faster)
container2.textContent = '<strong>Not parsed</strong>';
// Finding elements relative to another
const parent = document.querySelector('.parent');
const child = parent.querySelector('.child');
const ancestor = child.closest('.ancestor');
const sibling = child.nextElementSibling;
const prevSibling = child.previousElementSibling;
// Check if element matches selector
if (element.matches('.active')) {
console.log('Element is active');
}
Example: Efficient DOM manipulation
// BAD: Multiple reflows
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // Causes reflow each time
}
// GOOD: Use DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // Single reflow
// GOOD: Build HTML string and insert once
const items = Array.from({length: 100}, (_, i) => `
<div class="item">Item ${i}</div>
`).join('');
document.querySelector('.container').innerHTML = items;
// Helper function to create elements with properties
function createElement(tag, props = {}, children = []) {
const element = document.createElement(tag);
// Set properties
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'dataset') {
Object.assign(element.dataset, value);
} else if (key === 'style') {
Object.assign(element.style, value);
} else if (key.startsWith('on')) {
const event = key.slice(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
// Add children
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
}
// Usage
const button = createElement('button', {
className: 'btn btn-primary',
dataset: {userId: '123'},
onclick: () => console.log('Clicked'),
type: 'button'
}, ['Click Me']);
// Batch DOM reads and writes
// BAD: Interleaving reads and writes
elements.forEach(el => {
const height = el.offsetHeight; // Read
el.style.height = height + 10 + 'px'; // Write
});
// GOOD: Batch all reads, then all writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // All writes
});
// Template element for reusable DOM structures
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<h3 class="card-title"></h3>
<p class="card-body"></p>
<button class="card-btn">Action</button>
</div>
`;
function createCard(title, body) {
const clone = template.content.cloneNode(true);
clone.querySelector('.card-title').textContent = title;
clone.querySelector('.card-body').textContent = body;
return clone;
}
document.body.appendChild(createCard('Title', 'Body text'));
// Observing DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Type:', mutation.type);
console.log('Added:', mutation.addedNodes);
console.log('Removed:', mutation.removedNodes);
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true
});
// Stop observing
observer.disconnect();
Key Points: Use
querySelector() for flexible selection with CSS selectors.
querySelectorAll() returns static NodeList. classList for class
manipulation. textContent is safer than innerHTML. Use DocumentFragment for batch
inserts. Modern methods: append(), prepend(), before(),
after(), remove(). Access data attributes via element.dataset.
21.3 Event System and Event Delegation
Event Registration Methods
| Method | Syntax | Notes |
|---|---|---|
| addEventListener() | element.addEventListener(type, handler, options) | Recommended, multiple handlers allowed |
| removeEventListener() | element.removeEventListener(type, handler, options) | Remove specific handler |
| element.onclick | element.onclick = handler | Only one handler, overwrites previous |
| HTML attribute | <button onclick="handler()"> | Not recommended |
Event Listener Options
| Option | Type | Description |
|---|---|---|
| capture | boolean | Use capture phase (default: false) |
| once | boolean | Remove after first invocation |
| passive | boolean | Won't call preventDefault() (improves scroll performance) |
| signal | AbortSignal | Remove listener when signal aborted |
Common Event Types
| Category | Events | When Fired |
|---|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave | Mouse interactions |
| Keyboard | keydown, keyup, keypress (deprecated) | Keyboard input |
| Form | submit, change, input, focus, blur, invalid | Form interactions |
| Focus | focus, blur, focusin, focusout | Element focus changes |
| Touch | touchstart, touchmove, touchend, touchcancel | Touch device interactions |
| Drag | dragstart, drag, dragend, dragover, drop | Drag and drop |
| Document | DOMContentLoaded, load, beforeunload, unload | Document lifecycle |
| Clipboard | copy, cut, paste | Clipboard operations |
Event Object Properties
| Property | Type | Description |
|---|---|---|
| event.type | string | Event type ("click", "keydown", etc.) |
| event.target | Element | Element that triggered event |
| event.currentTarget | Element | Element with listener attached |
| event.bubbles | boolean | Whether event bubbles |
| event.cancelable | boolean | Whether can be cancelled |
| event.defaultPrevented | boolean | Whether preventDefault called |
| event.timeStamp | number | Time event created (ms) |
| event.isTrusted | boolean | True if user-generated |
Event Control Methods
| Method | Purpose | Effect |
|---|---|---|
| event.preventDefault() | Cancel default action | Prevents default browser behavior |
| event.stopPropagation() | Stop bubbling | Prevents event from propagating |
| event.stopImmediatePropagation() | Stop all propagation | Stops propagation and remaining handlers |
Example: Event handling basics
// Basic event listener
const button = document.querySelector('.btn');
button.addEventListener('click', (event) => {
console.log('Button clicked!');
console.log('Target:', event.target);
console.log('Current target:', event.currentTarget);
});
// Event listener with options
button.addEventListener('click', handler, {
once: true, // Remove after first call
capture: false, // Bubbling phase
passive: true // Won't call preventDefault
});
// Remove event listener
function handleClick(event) {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Using AbortController to remove listeners
const controller = new AbortController();
button.addEventListener('click', () => {
console.log('Clicked');
}, {signal: controller.signal});
// Remove listener
controller.abort();
// Multiple elements
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
console.log('Button clicked:', e.target.textContent);
});
});
// Prevent default behavior
const link = document.querySelector('a');
link.addEventListener('click', (event) => {
event.preventDefault(); // Don't navigate
console.log('Link clicked but not followed');
});
// Form submission
const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault(); // Don't submit to server
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log('Form data:', data);
// Handle form submission with fetch
fetch('/api/submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
});
// Keyboard events
const input = document.querySelector('input');
input.addEventListener('keydown', (event) => {
console.log('Key:', event.key);
console.log('Code:', event.code);
console.log('Ctrl:', event.ctrlKey);
console.log('Shift:', event.shiftKey);
console.log('Alt:', event.altKey);
// Check for specific key
if (event.key === 'Enter') {
console.log('Enter pressed');
}
// Check for key combination
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Prevent browser save
console.log('Ctrl+S pressed');
}
});
// Input event (fires on every change)
input.addEventListener('input', (event) => {
console.log('Current value:', event.target.value);
});
// Change event (fires when input loses focus)
input.addEventListener('change', (event) => {
console.log('Final value:', event.target.value);
});
// Mouse events
const box = document.querySelector('.box');
box.addEventListener('mouseenter', () => {
console.log('Mouse entered');
});
box.addEventListener('mouseleave', () => {
console.log('Mouse left');
});
box.addEventListener('mousemove', (event) => {
console.log('Mouse position:', event.clientX, event.clientY);
});
// Touch events
box.addEventListener('touchstart', (event) => {
console.log('Touch started');
console.log('Touches:', event.touches.length);
});
box.addEventListener('touchmove', (event) => {
event.preventDefault(); // Prevent scrolling
const touch = event.touches[0];
console.log('Touch position:', touch.clientX, touch.clientY);
}, {passive: false}); // Must be false to call preventDefault
// Document ready
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully loaded');
});
// Page load (including images, styles)
window.addEventListener('load', () => {
console.log('Page fully loaded');
});
// Before page unload
window.addEventListener('beforeunload', (event) => {
event.preventDefault();
event.returnValue = ''; // Show confirmation dialog
});
Example: Event delegation
// Event delegation - handle events on parent
const list = document.querySelector('.list');
// BAD: Add listener to each item (inefficient for many items)
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', handleClick);
});
// GOOD: Single listener on parent (event delegation)
list.addEventListener('click', (event) => {
// Find closest list item
const item = event.target.closest('.list-item');
if (item) {
console.log('Item clicked:', item.textContent);
item.classList.toggle('selected');
}
});
// More complex delegation
list.addEventListener('click', (event) => {
const target = event.target;
// Handle delete button
if (target.matches('.delete-btn')) {
const item = target.closest('.list-item');
item.remove();
}
// Handle edit button
if (target.matches('.edit-btn')) {
const item = target.closest('.list-item');
editItem(item);
}
// Handle item click (but not buttons)
if (target.matches('.list-item') && !target.closest('button')) {
toggleItem(target);
}
});
// Delegation helper function
function delegate(parent, selector, eventType, handler) {
parent.addEventListener(eventType, (event) => {
const target = event.target.closest(selector);
if (target && parent.contains(target)) {
handler.call(target, event);
}
});
}
// Usage
delegate(list, '.list-item', 'click', function(event) {
console.log('Item clicked:', this.textContent);
});
// Dynamic content with delegation
function addItem(text) {
const item = document.createElement('div');
item.className = 'list-item';
item.innerHTML = `
<span>${text}</span>
<button class="delete-btn">Delete</button>
`;
list.appendChild(item);
// No need to add new event listeners!
}
// Event bubbling example
document.querySelector('.grandparent').addEventListener('click', () => {
console.log('Grandparent clicked');
});
document.querySelector('.parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.querySelector('.child').addEventListener('click', (event) => {
console.log('Child clicked');
// Stop propagation to parent
// event.stopPropagation();
});
// Clicking child logs: "Child clicked", "Parent clicked", "Grandparent clicked"
// Event capture phase
document.querySelector('.grandparent').addEventListener('click', () => {
console.log('Grandparent (capture)');
}, true); // Capture phase
document.querySelector('.parent').addEventListener('click', () => {
console.log('Parent (capture)');
}, true);
document.querySelector('.child').addEventListener('click', () => {
console.log('Child (bubble)');
});
// Order: "Grandparent (capture)", "Parent (capture)", "Child (bubble)"
// Custom event dispatcher
class EventBus {
constructor() {
this.events = {};
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
off(event, handler) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(h => h !== handler);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(handler => {
handler(data);
});
}
}
// Usage
const bus = new EventBus();
bus.on('user:login', (user) => {
console.log('User logged in:', user);
});
bus.emit('user:login', {name: 'John', id: 123});
// Custom DOM events
const customEvent = new CustomEvent('myevent', {
detail: {message: 'Hello'},
bubbles: true,
cancelable: true
});
element.addEventListener('myevent', (event) => {
console.log('Custom event:', event.detail);
});
element.dispatchEvent(customEvent);
Key Points: Use
addEventListener() for flexibility. Event
delegation: attach listener to parent, check event.target. Use event.preventDefault() to
cancel default action. event.stopPropagation() stops bubbling. once: true for
one-time listeners. AbortController for cleanup. passive: true for scroll performance.
Event phases: capture → target → bubble.
21.4 Storage APIs (localStorage, sessionStorage, IndexedDB)
Web Storage API (localStorage & sessionStorage)
| Method | Purpose | Returns |
|---|---|---|
| setItem(key, value) | Store key-value pair | undefined |
| getItem(key) | Retrieve value | string | null |
| removeItem(key) | Remove item | undefined |
| clear() | Remove all items | undefined |
| key(index) | Get key at index | string | null |
| length | Number of items | number |
localStorage vs sessionStorage
| Feature | localStorage | sessionStorage |
|---|---|---|
| Persistence | Until manually cleared | Until tab/window closed |
| Scope | Shared across tabs | Per tab/window |
| Storage limit | ~5-10 MB | ~5-10 MB |
| Data type | Strings only | Strings only |
| Use case | Long-term data, preferences | Session data, temporary state |
IndexedDB Key Concepts
| Concept | Description | Example |
|---|---|---|
| Database | Container for object stores | indexedDB.open('myDB', 1) |
| Object Store | Like a table in SQL | db.createObjectStore('users') |
| Transaction | Atomic operation wrapper | db.transaction(['users'], 'readwrite') |
| Index | Query by non-key property | store.createIndex('email', 'email') |
| Key | Unique identifier | autoIncrement or keyPath |
| Cursor | Iterate over records | store.openCursor() |
IndexedDB Transaction Modes
| Mode | Access | When to Use |
|---|---|---|
| readonly | Read only | Queries, retrieving data |
| readwrite | Read and write | Creating, updating, deleting |
| versionchange | Schema changes | onupgradeneeded event |
Example: localStorage and sessionStorage
// localStorage - persists across sessions
// Store data
localStorage.setItem('username', 'john_doe');
localStorage.setItem('theme', 'dark');
// Retrieve data
const username = localStorage.getItem('username'); // 'john_doe'
const theme = localStorage.getItem('theme'); // 'dark'
// Store objects (must stringify)
const user = {
id: 123,
name: 'John Doe',
email: 'john@example.com'
};
localStorage.setItem('user', JSON.stringify(user));
// Retrieve and parse
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // 'John Doe'
// Remove item
localStorage.removeItem('theme');
// Clear all
localStorage.clear();
// Check if key exists
if (localStorage.getItem('username')) {
console.log('Username exists');
}
// Iterate over all items
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(key, value);
}
// sessionStorage - cleared when tab closes
sessionStorage.setItem('tempData', 'temporary');
// Storage helper class
class Storage {
constructor(storage = localStorage) {
this.storage = storage;
}
set(key, value) {
try {
this.storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
get(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Parse error:', error);
return defaultValue;
}
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
has(key) {
return this.storage.getItem(key) !== null;
}
keys() {
return Object.keys(this.storage);
}
}
// Usage
const store = new Storage();
store.set('user', {name: 'John', age: 30});
const user2 = store.get('user');
console.log(user2.name); // 'John'
// Listen for storage changes (in other tabs)
window.addEventListener('storage', (event) => {
console.log('Storage changed:');
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
});
// Storage with expiration
class StorageWithExpiry {
set(key, value, ttl) {
const item = {
value: value,
expiry: Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}
get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) {
return null;
}
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
}
// Usage
const storage = new StorageWithExpiry();
// Store for 1 hour
storage.set('session', {token: 'abc123'}, 60 * 60 * 1000);
// Later...
const session = storage.get('session'); // null if expired
// Handle quota exceeded errors
try {
localStorage.setItem('large-data', 'x'.repeat(10 * 1024 * 1024));
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
// Clear old data or notify user
}
}
Example: IndexedDB basic operations
// Open database
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDatabase', 1);
// Create/upgrade schema
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// Create indexes
store.createIndex('email', 'email', {unique: true});
store.createIndex('name', 'name', {unique: false});
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// Add data
async function addUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => {
resolve(request.result); // Returns generated key
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get data by key
async function getUser(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get data by index
async function getUserByEmail(email) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get all data
async function getAllUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Update data
async function updateUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user); // Updates or inserts
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Delete data
async function deleteUser(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
// Usage
await addUser({name: 'John Doe', email: 'john@example.com'});
const user3 = await getUser(1);
const userByEmail = await getUserByEmail('john@example.com');
await updateUser({id: 1, name: 'John Smith', email: 'john@example.com'});
await deleteUser(1);
// Cursor iteration
async function iterateUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue(); // Move to next
} else {
resolve(results); // Done
}
};
request.onerror = () => {
reject(request.error);
};
});
}
// IndexedDB wrapper class
class IDBWrapper {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open(upgrade) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
if (upgrade) {
upgrade(event.target.result);
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = () => reject(request.error);
});
}
async add(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Usage
const db2 = new IDBWrapper('myApp', 1);
await db2.open((db) => {
db.createObjectStore('users', {keyPath: 'id', autoIncrement: true});
});
await db2.add('users', {name: 'John'});
const users = await db2.getAll('users');
Key Points:
localStorage persists, sessionStorage clears on tab
close.
Both store strings only - use JSON.stringify/parse for objects. IndexedDB for
large/complex data (NoSQL database). Use transactions for IndexedDB operations. Create indexes for efficient
queries. Handle QuotaExceededError. storage event for cross-tab communication.
21.5 Clipboard API and File API
Clipboard API Methods
| Method | Purpose | Returns |
|---|---|---|
| navigator.clipboard.writeText() | Copy text to clipboard | Promise<void> |
| navigator.clipboard.readText() | Read text from clipboard | Promise<string> |
| navigator.clipboard.write() | Copy data (images, HTML) to clipboard | Promise<void> |
| navigator.clipboard.read() | Read data from clipboard | Promise<ClipboardItem[]> |
File Input Properties
| Property | Type | Description |
|---|---|---|
| input.files | FileList | Selected files |
| input.accept | string | File type filter (e.g., "image/*") |
| input.multiple | boolean | Allow multiple file selection |
File Object Properties
| Property | Type | Description |
|---|---|---|
| file.name | string | File name |
| file.size | number | Size in bytes |
| file.type | string | MIME type (e.g., "image/png") |
| file.lastModified | number | Last modified timestamp |
FileReader Methods
| Method | Result Format | Use Case |
|---|---|---|
| readAsText() | String | Text files |
| readAsDataURL() | Data URL (base64) | Display images, download links |
| readAsArrayBuffer() | ArrayBuffer | Binary data, manipulation |
| readAsBinaryString() | Binary string (deprecated) | Legacy support |
Drag and Drop Events
| Event | Fires On | Usage |
|---|---|---|
| dragstart | Start dragging | Set drag data |
| drag | During drag | Track drag position |
| dragenter | Enter drop zone | Visual feedback |
| dragover | Over drop zone | Allow drop (preventDefault) |
| dragleave | Leave drop zone | Remove feedback |
| drop | Dropped | Handle dropped data |
| dragend | Drag finished | Cleanup |
Example: Clipboard API
// Copy text to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Copied to clipboard');
} catch (error) {
console.error('Failed to copy:', error);
}
}
// Usage
await copyToClipboard('Hello, World!');
// Read text from clipboard
async function readFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('Clipboard text:', text);
return text;
} catch (error) {
console.error('Failed to read:', error);
}
}
// Copy button
document.querySelector('.copy-btn').addEventListener('click', async () => {
const text = document.querySelector('.code').textContent;
await copyToClipboard(text);
});
// Copy image to clipboard
async function copyImageToClipboard(blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied');
} catch (error) {
console.error('Failed to copy image:', error);
}
}
// Fetch and copy image
async function copyImageFromUrl(url) {
const response = await fetch(url);
const blob = await response.blob();
await copyImageToClipboard(blob);
}
// Fallback for older browsers
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
console.log('Copy:', successful ? 'success' : 'failed');
} catch (error) {
console.error('Fallback copy failed:', error);
}
document.body.removeChild(textarea);
}
// Copy with fallback
async function copyText(text) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
fallbackCopy(text);
}
}
// Listen for copy event
document.addEventListener('copy', (event) => {
// Customize copied content
const selection = document.getSelection().toString();
const customText = `${selection}\n\nSource: ${window.location.href}`;
event.clipboardData.setData('text/plain', customText);
event.preventDefault();
});
// Listen for paste event
document.addEventListener('paste', (event) => {
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
console.log('Pasted:', text);
// Insert at cursor or handle paste
document.execCommand('insertText', false, text);
});
Example: File API - reading files
// HTML: <input type="file" id="fileInput" multiple accept="image/*">
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
const files = event.target.files;
for (const file of files) {
console.log('Name:', file.name);
console.log('Size:', file.size, 'bytes');
console.log('Type:', file.type);
console.log('Last modified:', new Date(file.lastModified));
}
});
// Read text file
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
console.log('File content:', text);
};
reader.onerror = (e) => {
console.error('Error reading file:', e);
};
reader.readAsText(file);
}
});
// Read image and display
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result; // Data URL
document.body.appendChild(img);
};
reader.readAsDataURL(file);
}
});
// Read as ArrayBuffer for binary processing
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const arrayBuffer = await file.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
console.log('First 10 bytes:', bytes.slice(0, 10));
}
});
// Progress tracking
function readFileWithProgress(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`Progress: ${percent.toFixed(2)}%`);
}
};
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (event) => {
reject(reader.error);
};
reader.readAsArrayBuffer(file);
});
}
// Multiple files
fileInput.addEventListener('change', async (event) => {
const files = Array.from(event.target.files);
const promises = files.map(file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve({file, data: e.target.result});
reader.onerror = reject;
reader.readAsDataURL(file);
});
});
const results = await Promise.all(promises);
results.forEach(({file, data}) => {
console.log(`${file.name}: ${data.length} characters`);
});
});
// File validation
function validateFile(file) {
const maxSize = 5 * 1024 * 1024; // 5 MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
return true;
}
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
try {
validateFile(file);
// Process file
} catch (error) {
alert(error.message);
fileInput.value = ''; // Clear input
}
});
Example: Drag and drop file upload
// HTML: <div id="dropZone">Drop files here</div>
const dropZone = document.getElementById('dropZone');
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Visual feedback
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.add('highlight');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.remove('highlight');
}, false);
});
// Handle drop
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
function handleFiles(files) {
[...files].forEach(uploadFile);
}
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Upload success:', data);
})
.catch(error => {
console.error('Upload error:', error);
});
}
// Preview dropped images
function handleDrop2(e) {
const dt = e.dataTransfer;
const files = dt.files;
[...files].forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '200px';
dropZone.appendChild(img);
};
reader.readAsDataURL(file);
}
});
}
// Drag from element to another
const draggableItem = document.querySelector('.draggable');
draggableItem.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.innerHTML);
e.dataTransfer.setData('text/plain', e.target.textContent);
});
const dropTarget = document.querySelector('.drop-target');
dropTarget.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
dropTarget.addEventListener('drop', (e) => {
e.preventDefault();
const html = e.dataTransfer.getData('text/html');
dropTarget.innerHTML = html;
});
// File System Access API (modern browsers)
async function openFilePicker() {
try {
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Text Files',
accept: {'text/plain': ['.txt']}
}],
multiple: false
});
const file = await fileHandle.getFile();
const text = await file.text();
console.log('File content:', text);
} catch (error) {
console.error('Error:', error);
}
}
// Save file
async function saveFile(content) {
try {
const fileHandle = await window.showSaveFilePicker({
suggestedName: 'document.txt',
types: [{
description: 'Text Files',
accept: {'text/plain': ['.txt']}
}]
});
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
console.log('File saved');
} catch (error) {
console.error('Error:', error);
}
}
Key Points:
navigator.clipboard.writeText() for copying text (requires HTTPS).
Use FileReader to read file contents (text, dataURL, arrayBuffer). File
validation
(size, type) before upload. Drag and drop: preventDefault on dragover and drop. FormData for file uploads.
file.arrayBuffer() for modern async file reading. File System Access API for advanced file
operations.
21.6 Geolocation and Device APIs
Geolocation API Methods
| Method | Purpose | Callback Parameters |
|---|---|---|
| getCurrentPosition() | Get current location once | success, error, options |
| watchPosition() | Track location changes | success, error, options |
| clearWatch() | Stop watching position | watchId |
Position Object Properties
| Property | Type | Description |
|---|---|---|
| coords.latitude | number | Latitude in decimal degrees |
| coords.longitude | number | Longitude in decimal degrees |
| coords.accuracy | number | Accuracy in meters |
| coords.altitude | number | null | Altitude in meters |
| coords.altitudeAccuracy | number | null | Altitude accuracy in meters |
| coords.heading | number | null | Direction in degrees (0=North) |
| coords.speed | number | null | Speed in meters per second |
| timestamp | number | Timestamp of position |
Geolocation Options
| Option | Type | Description |
|---|---|---|
| enableHighAccuracy | boolean | Request best possible results (uses GPS) |
| timeout | number | Maximum time to wait (ms) |
| maximumAge | number | Max age of cached position (ms) |
Device Orientation Properties
| Property | Range | Description |
|---|---|---|
| alpha | 0-360 | Z-axis rotation (compass heading) |
| beta | -180 to 180 | X-axis rotation (front-to-back tilt) |
| gamma | -90 to 90 | Y-axis rotation (left-to-right tilt) |
| absolute | boolean | True if relative to Earth's coordinate frame |
Other Device APIs
| API | Purpose | Access Via |
|---|---|---|
| Battery Status | Battery level and charging status | navigator.getBattery() |
| Network Information | Connection type and speed | navigator.connection |
| Vibration | Trigger device vibration | navigator.vibrate() |
| Screen Orientation | Lock/unlock screen orientation | screen.orientation |
| Media Devices | Access camera/microphone | navigator.mediaDevices |
Example: Geolocation API
// Get current position
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
console.log('Accuracy:', position.coords.accuracy, 'meters');
console.log('Timestamp:', new Date(position.timestamp));
},
(error) => {
console.error('Error:', error.code, error.message);
switch(error.code) {
case error.PERMISSION_DENIED:
console.log('User denied geolocation');
break;
case error.POSITION_UNAVAILABLE:
console.log('Location unavailable');
break;
case error.TIMEOUT:
console.log('Request timeout');
break;
}
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
} else {
console.log('Geolocation not supported');
}
// Async/await wrapper
function getPosition(options) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
}
// Usage
try {
const position = await getPosition({
enableHighAccuracy: true,
timeout: 10000
});
const {latitude, longitude} = position.coords;
console.log(`Location: ${latitude}, ${longitude}`);
} catch (error) {
console.error('Geolocation error:', error);
}
// Watch position (continuous tracking)
const watchId = navigator.geolocation.watchPosition(
(position) => {
const {latitude, longitude, speed, heading} = position.coords;
console.log(`Lat: ${latitude}, Lng: ${longitude}`);
if (speed !== null) {
console.log(`Speed: ${speed} m/s`);
}
if (heading !== null) {
console.log(`Heading: ${heading}°`);
}
// Update map or UI
updateMap(latitude, longitude);
},
(error) => {
console.error('Watch error:', error);
},
{
enableHighAccuracy: true,
maximumAge: 30000,
timeout: 27000
}
);
// Stop watching
navigator.geolocation.clearWatch(watchId);
// Calculate distance between two points (Haversine formula)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Earth's radius in meters
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
// Usage
const distance = calculateDistance(51.5074, -0.1278, 48.8566, 2.3522);
console.log(`Distance: ${(distance / 1000).toFixed(2)} km`);
// Geolocation with React hook
function useGeolocation(options) {
const [position, setPosition] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const watchId = navigator.geolocation.watchPosition(
setPosition,
setError,
options
);
return () => navigator.geolocation.clearWatch(watchId);
}, []);
return {position, error};
}
// Usage in component
function LocationTracker() {
const {position, error} = useGeolocation({enableHighAccuracy: true});
if (error) return <div>Error: {error.message}</div>;
if (!position) return <div>Loading...</div>;
return (
<div>
<p>Lat: {position.coords.latitude}</p>
<p>Lng: {position.coords.longitude}</p>
</div>
);
}
Example: Device orientation and other APIs
// Device orientation (gyroscope)
window.addEventListener('deviceorientation', (event) => {
const alpha = event.alpha; // Z-axis (0-360)
const beta = event.beta; // X-axis (-180 to 180)
const gamma = event.gamma; // Y-axis (-90 to 90)
console.log(`α: ${alpha}°, β: ${beta}°, γ: ${gamma}°`);
// Use for compass, level, or 3D effects
updateCompass(alpha);
});
// Device motion (accelerometer)
window.addEventListener('devicemotion', (event) => {
const acceleration = event.acceleration;
const rotationRate = event.rotationRate;
console.log('Acceleration:');
console.log(' x:', acceleration.x);
console.log(' y:', acceleration.y);
console.log(' z:', acceleration.z);
console.log('Rotation rate:');
console.log(' α:', rotationRate.alpha);
console.log(' β:', rotationRate.beta);
console.log(' γ:', rotationRate.gamma);
// Detect shake
const totalAcceleration = Math.abs(acceleration.x) +
Math.abs(acceleration.y) +
Math.abs(acceleration.z);
if (totalAcceleration > 30) {
console.log('Device shaken!');
}
});
// Request permission for iOS 13+
async function requestMotionPermission() {
if (typeof DeviceMotionEvent.requestPermission === 'function') {
const permission = await DeviceMotionEvent.requestPermission();
if (permission === 'granted') {
window.addEventListener('devicemotion', handleMotion);
}
}
}
// Battery API
navigator.getBattery().then((battery) => {
console.log('Battery level:', battery.level * 100, '%');
console.log('Charging:', battery.charging);
console.log('Charging time:', battery.chargingTime, 'seconds');
console.log('Discharging time:', battery.dischargingTime, 'seconds');
// Listen for changes
battery.addEventListener('levelchange', () => {
console.log('Battery level:', battery.level * 100, '%');
});
battery.addEventListener('chargingchange', () => {
console.log('Charging:', battery.charging);
});
});
// Network Information API
if ('connection' in navigator) {
const connection = navigator.connection;
console.log('Connection type:', connection.effectiveType);
console.log('Downlink speed:', connection.downlink, 'Mb/s');
console.log('RTT:', connection.rtt, 'ms');
console.log('Save data:', connection.saveData);
// Adjust quality based on connection
if (connection.effectiveType === '4g') {
loadHighQualityImages();
} else {
loadLowQualityImages();
}
// Listen for connection changes
connection.addEventListener('change', () => {
console.log('Connection changed to:', connection.effectiveType);
});
}
// Vibration API
if ('vibrate' in navigator) {
// Vibrate for 200ms
navigator.vibrate(200);
// Pattern: vibrate, pause, vibrate
navigator.vibrate([200, 100, 200]);
// Stop vibration
navigator.vibrate(0);
// Feedback on button click
button.addEventListener('click', () => {
navigator.vibrate(50);
});
}
// Screen Orientation API
if ('orientation' in screen) {
console.log('Current orientation:', screen.orientation.type);
console.log('Angle:', screen.orientation.angle);
// Lock orientation
screen.orientation.lock('landscape').then(() => {
console.log('Orientation locked');
}).catch((error) => {
console.error('Lock failed:', error);
});
// Unlock orientation
screen.orientation.unlock();
// Listen for orientation changes
screen.orientation.addEventListener('change', () => {
console.log('New orientation:', screen.orientation.type);
});
}
// Media Devices (Camera/Microphone)
async function getUserMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: {ideal: 1920},
height: {ideal: 1080}
},
audio: true
});
// Display video
const video = document.querySelector('video');
video.srcObject = stream;
return stream;
} catch (error) {
console.error('Media access denied:', error);
}
}
// List available devices
async function listDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach(device => {
console.log(device.kind, ':', device.label);
});
}
// Online/offline detection
window.addEventListener('online', () => {
console.log('Back online');
syncOfflineData();
});
window.addEventListener('offline', () => {
console.log('Gone offline');
showOfflineMessage();
});
// Check current status
if (navigator.onLine) {
console.log('Currently online');
} else {
console.log('Currently offline');
}
// Page Visibility API
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page hidden');
pauseVideo();
} else {
console.log('Page visible');
resumeVideo();
}
});
Key Points: Geolocation API requires user permission. Use
enableHighAccuracy for GPS. watchPosition() for continuous tracking. Device
orientation
for gyroscope data. Battery API for power management. Network Information to adapt to connection quality.
Vibration API for haptic feedback. Always check for API support before use.
21.7 Web Workers and Service Workers
Web Worker Types
| Type | Scope | Use Case |
|---|---|---|
| Dedicated Worker | Single script | Heavy computation, background tasks |
| Shared Worker | Multiple scripts/tabs | Cross-tab communication |
| Service Worker | Network proxy | Offline support, caching, push notifications |
Worker Methods and Events
| Method/Event | Purpose | Context |
|---|---|---|
| postMessage() | Send message | Main thread ↔ Worker |
| onmessage | Receive message | Main thread ↔ Worker |
| terminate() | Stop worker | Main thread |
| close() | Stop self | Worker |
| onerror | Handle errors | Main thread |
| importScripts() | Load external scripts | Worker |
Service Worker Lifecycle
| State | Description | Event |
|---|---|---|
| Installing | First time registration | install |
| Installed (Waiting) | Installed but not active | - |
| Activating | Taking control | activate |
| Activated | Controlling pages | - |
| Redundant | Replaced by newer version | - |
Service Worker Events
| Event | When Fired | Use Case |
|---|---|---|
| install | First installation | Cache resources |
| activate | Becomes active | Clean old caches |
| fetch | Network request | Cache strategy, offline fallback |
| message | Receive message from page | Communication |
| push | Push notification received | Show notification |
| sync | Background sync triggered | Sync data when online |
Cache Strategies
| Strategy | Description | Best For |
|---|---|---|
| Cache First | Cache, fallback to network | Static assets (CSS, JS, images) |
| Network First | Network, fallback to cache | API calls, dynamic content |
| Cache Only | Always from cache | Offline-first apps |
| Network Only | Always from network | Real-time data |
| Stale While Revalidate | Cache first, update in background | Balance freshness and speed |
Example: Web Workers
// Main thread (main.js)
const worker = new Worker('worker.js');
// Send message to worker
worker.postMessage({
type: 'calculate',
data: [1, 2, 3, 4, 5]
});
// Receive message from worker
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate worker
worker.terminate();
// Worker file (worker.js)
self.onmessage = (event) => {
const {type, data} = event.data;
if (type === 'calculate') {
// Heavy computation
const result = data.reduce((sum, num) => sum + num, 0);
// Send result back
self.postMessage({
type: 'result',
result: result
});
}
};
// Import external scripts in worker
importScripts('math-utils.js', 'helpers.js');
// Example: Image processing in worker
// Main thread
const imageWorker = new Worker('image-processor.js');
canvas.toBlob((blob) => {
imageWorker.postMessage({
type: 'process',
image: blob
});
});
imageWorker.onmessage = (event) => {
const processedImage = event.data;
displayImage(processedImage);
};
// Worker (image-processor.js)
self.onmessage = async (event) => {
const {type, image} = event.data;
if (type === 'process') {
// Process image
const bitmap = await createImageBitmap(image);
// Apply filters, transformations
const processed = applyFilters(bitmap);
self.postMessage(processed);
}
};
// Transferable objects (zero-copy transfer)
const buffer = new ArrayBuffer(1024);
worker.postMessage(buffer, [buffer]); // Transfer ownership
// buffer is now unusable in main thread
// Worker pool for parallel processing
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = [];
this.queue = [];
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.busy = false;
worker.onmessage = (event) => {
worker.busy = false;
worker.currentTask.resolve(event.data);
this.processQueue();
};
worker.onerror = (error) => {
worker.busy = false;
worker.currentTask.reject(error);
this.processQueue();
};
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
this.queue.push({data, resolve, reject});
this.processQueue();
});
}
processQueue() {
if (this.queue.length === 0) return;
const availableWorker = this.workers.find(w => !w.busy);
if (availableWorker) {
const task = this.queue.shift();
availableWorker.busy = true;
availableWorker.currentTask = task;
availableWorker.postMessage(task.data);
}
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Usage
const pool = new WorkerPool('worker.js', 4);
const results = await Promise.all([
pool.run({task: 'process', data: data1}),
pool.run({task: 'process', data: data2}),
pool.run({task: 'process', data: data3})
]);
pool.terminate();
Example: Service Workers
// Register service worker (main.js)
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('New version available!');
// Prompt user to refresh
}
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
}
});
}
// Service Worker file (sw.js)
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js',
'/images/logo.png'
];
// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Caching files');
return cache.addAll(urlsToCache);
})
);
// Skip waiting to activate immediately
self.skipWaiting();
});
// Activate event - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// Take control of all pages immediately
self.clients.claim();
});
// Fetch event - serve from cache or network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// Clone request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Check valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone response
const responseToCache = response.clone();
// Cache new response
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// Network First strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache successful response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, responseToCache));
return response;
})
.catch(() => {
// Network failed, try cache
return caches.match(event.request);
})
);
});
// Stale While Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
const fetchPromise = fetch(event.request)
.then((networkResponse) => {
// Update cache
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, networkResponse.clone()));
return networkResponse;
});
// Return cached response immediately, update in background
return cachedResponse || fetchPromise;
})
);
});
// Message handling
self.addEventListener('message', (event) => {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/icon.png',
badge: '/images/badge.png',
data: {url: data.url}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
// Get pending messages from IndexedDB
const messages = await getPendingMessages();
// Send to server
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
// Remove from pending
await removePendingMessage(message.id);
} catch (error) {
console.error('Sync failed:', error);
}
}
}
// Unregister service worker
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => {
registration.unregister();
});
});
Key Points: Web Workers run on separate thread for heavy
computation.
Use
postMessage() for communication. Service Workers act as network
proxy for offline support. Cache strategies: cache-first, network-first, stale-while-revalidate. Service worker
lifecycle: install → activate. Handle fetch events to intercept requests. Push notifications and background
sync.
Transferable objects for efficient data transfer.
Section 21 Summary: Web APIs and Browser Integration
- Fetch API: Promise-based HTTP requests, response.json(), request options (method, headers, body)
- Response Handling: Check response.ok, status codes, AbortController for cancellation
- DOM Selection: querySelector/All, getElementById, closest, matches for flexible selection
- DOM Manipulation: createElement, append/prepend, classList, innerHTML vs textContent
- Events: addEventListener with options (once, capture, passive), event delegation pattern
- Storage: localStorage (persistent), sessionStorage (tab-scoped), IndexedDB (NoSQL database)
- Clipboard API: navigator.clipboard.writeText/readText, requires HTTPS and permissions
- File API: FileReader (readAsText, readAsDataURL), drag and drop, file validation
- Geolocation: getCurrentPosition, watchPosition, coordinates (lat/lng/accuracy)
- Device APIs: Orientation, motion, battery, network info, vibration, media devices
- Web Workers: Separate thread for computation, postMessage communication, worker pools
- Service Workers: Network proxy, offline support, caching strategies, push notifications
22. Modern JavaScript Patterns and Techniques
22.1 Module Patterns and Namespace Management
Module Pattern Types
| Pattern | Purpose | Usage |
|---|---|---|
| IIFE Module | Encapsulation with closure | Pre-ES6 modules |
| Revealing Module | Explicit public API | Clear interface definition |
| ES6 Modules | Native import/export | Modern JavaScript standard |
| CommonJS | Node.js modules | require/module.exports |
| Namespace Object | Group related functionality | Avoid global pollution |
ES6 Module Syntax
| Syntax | Description | Example |
|---|---|---|
| export | Named export | export const fn = () => {} |
| export default | Default export | export default class {} |
| import | Import named exports | import {fn} from './module' |
| import default | Import default export | import MyClass from './module' |
| import * as | Import namespace | import * as utils from './utils' |
| export {} | Re-export | export {fn} from './module' |
Module Benefits
| Benefit | Description | Impact |
|---|---|---|
| Encapsulation | Hide internal implementation | Prevent unwanted access |
| Reusability | Share code across projects | DRY principle |
| Maintainability | Organized code structure | Easier to debug and update |
| Dependency Management | Explicit dependencies | Clear module relationships |
| Namespace | Avoid global scope pollution | Prevent naming conflicts |
Example: Module patterns
// IIFE Module Pattern (pre-ES6)
const myModule = (function() {
// Private variables and functions
let privateVar = 'I am private';
function privateMethod() {
console.log(privateVar);
}
// Public API
return {
publicMethod: function() {
privateMethod();
},
publicVar: 'I am public'
};
})();
myModule.publicMethod(); // Works
// myModule.privateVar // undefined (not accessible)
// Revealing Module Pattern
const calculator = (function() {
// Private
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
// Reveal public API
return {
add: add,
subtract: subtract
// multiply is private
};
})();
calculator.add(2, 3); // 5
calculator.subtract(5, 2); // 3
// calculator.multiply(2, 3) // Error: not exposed
// Namespace Pattern
const MyApp = MyApp || {};
MyApp.Utils = {
formatDate: function(date) {
return date.toLocaleDateString();
},
formatCurrency: function(amount) {
return `${amount.toFixed(2)}`;
}
};
MyApp.Models = {
User: class {
constructor(name) {
this.name = name;
}
}
};
MyApp.Controllers = {
UserController: class {
createUser(name) {
return new MyApp.Models.User(name);
}
}
};
// Usage
const formatted = MyApp.Utils.formatCurrency(99.99);
const user = new MyApp.Models.User('John');
// ES6 Modules
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// Default export
export default function multiply(a, b) {
return a * b;
}
// Import in another file
import multiply, {add, subtract, PI} from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6
console.log(PI); // 3.14159
// Import all as namespace
import * as math from './math.js';
console.log(math.add(2, 3));
console.log(math.default(2, 3)); // multiply
// Re-export pattern (index.js)
export {add, subtract} from './math.js';
export {User} from './user.js';
export {default as multiply} from './math.js';
// Lazy loading modules
async function loadModule() {
const module = await import('./heavy-module.js');
module.doSomething();
}
// Module with initialization
// logger.js
class Logger {
constructor() {
this.logs = [];
}
log(message) {
this.logs.push({
message,
timestamp: Date.now()
});
console.log(message);
}
getLogs() {
return this.logs;
}
}
// Export singleton instance
export default new Logger();
// Usage
import logger from './logger.js';
logger.log('Application started');
logger.log('User logged in');
// Dependency injection with modules
// services.js
export class ApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`);
return response.json();
}
}
export class AuthService {
constructor(apiService) {
this.api = apiService;
}
async login(credentials) {
return this.api.get('/auth/login', credentials);
}
}
// app.js
import {ApiService, AuthService} from './services.js';
const api = new ApiService('https://api.example.com');
const auth = new AuthService(api);
await auth.login({username: 'john', password: 'pass123'});
Key Points: Module pattern uses IIFE for encapsulation.
Revealing
module pattern explicitly defines public API. ES6 modules with
import/export are standard. Use
export default for main export. Namespace objects prevent global pollution. Dynamic imports for
lazy loading. Modules provide encapsulation, reusability, maintainability.
22.2 Observer Pattern and Event Emitters
Observer Pattern Components
| Component | Role | Responsibility |
|---|---|---|
| Subject | Observable | Maintains observers list, notifies on change |
| Observer | Subscriber | Receives notifications, reacts to changes |
| ConcreteSubject | Specific observable | Stores state, triggers notifications |
| ConcreteObserver | Specific subscriber | Implements update logic |
Event Emitter Methods
| Method | Purpose | Usage |
|---|---|---|
| on(event, handler) | Register listener | Subscribe to event |
| off(event, handler) | Remove listener | Unsubscribe from event |
| emit(event, data) | Trigger event | Notify all listeners |
| once(event, handler) | One-time listener | Auto-remove after first call |
| removeAllListeners() | Clear all listeners | Cleanup |
Observer Pattern Benefits
| Benefit | Description | Use Case |
|---|---|---|
| Loose Coupling | Subject and observers independent | Flexible system architecture |
| Dynamic Relationships | Add/remove observers at runtime | Plugins, extensions |
| Broadcast Communication | One-to-many notification | State changes, events |
| Separation of Concerns | Observers handle own logic | Clean code organization |
Example: Observer pattern and Event Emitter
// Classic Observer Pattern
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
}
}
detach(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach(observer => {
observer.update(data);
});
}
}
class Observer {
update(data) {
console.log('Observer received:', data);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.attach(observer1);
subject.attach(observer2);
subject.notify({message: 'Hello observers!'});
subject.detach(observer1);
subject.notify({message: 'Only observer2 gets this'});
// Event Emitter Pattern
class EventEmitter {
constructor() {
this.events = {};
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off(event, handler) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(h => h !== handler);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(handler => {
handler(data);
});
}
once(event, handler) {
const onceHandler = (data) => {
handler(data);
this.off(event, onceHandler);
};
this.on(event, onceHandler);
}
removeAllListeners(event) {
if (event) {
delete this.events[event];
} else {
this.events = {};
}
}
listenerCount(event) {
return this.events[event] ? this.events[event].length : 0;
}
}
// Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on('data', (data) => {
console.log('Data received:', data);
});
emitter.emit('data', {value: 42});
// One-time listener
emitter.once('complete', () => {
console.log('Completed!');
});
emitter.emit('complete'); // Logs "Completed!"
emitter.emit('complete'); // Nothing happens
// Unsubscribe
unsubscribe();
// Real-world example: Store with observers
class Store extends EventEmitter {
constructor(initialState = {}) {
super();
this.state = initialState;
}
getState() {
return this.state;
}
setState(updates) {
const prevState = {...this.state};
this.state = {...this.state, ...updates};
this.emit('change', {
state: this.state,
prevState: prevState
});
}
subscribe(handler) {
return this.on('change', handler);
}
}
// Usage
const store = new Store({count: 0, user: null});
const unsubscribeStore = store.subscribe(({state, prevState}) => {
console.log('State changed from', prevState, 'to', state);
});
store.setState({count: 1});
store.setState({user: {name: 'John'}});
// Real-world: UI Component with events
class Button extends EventEmitter {
constructor(label) {
super();
this.label = label;
this.element = null;
}
render() {
this.element = document.createElement('button');
this.element.textContent = this.label;
this.element.addEventListener('click', () => {
this.emit('click', {label: this.label});
});
return this.element;
}
setText(label) {
this.label = label;
if (this.element) {
this.element.textContent = label;
}
this.emit('textChange', {label});
}
}
// Usage
const button = new Button('Click Me');
button.on('click', ({label}) => {
console.log(`Button "${label}" clicked`);
});
button.on('textChange', ({label}) => {
console.log(`Button text changed to "${label}"`);
});
document.body.appendChild(button.render());
// Async event emitter
class AsyncEventEmitter extends EventEmitter {
async emit(event, data) {
if (!this.events[event]) return;
await Promise.all(
this.events[event].map(handler => handler(data))
);
}
}
const asyncEmitter = new AsyncEventEmitter();
asyncEmitter.on('fetch', async (url) => {
const response = await fetch(url);
console.log('Fetched:', response.status);
});
await asyncEmitter.emit('fetch', 'https://api.example.com/data');
// Event namespacing
class NamespacedEventEmitter extends EventEmitter {
emit(event, data) {
// Emit specific event
super.emit(event, data);
// Emit namespace events
const parts = event.split(':');
for (let i = 1; i < parts.length; i++) {
const namespace = parts.slice(0, i).join(':');
super.emit(namespace, data);
}
}
}
const nsEmitter = new NamespacedEventEmitter();
nsEmitter.on('user', (data) => {
console.log('Any user event:', data);
});
nsEmitter.on('user:login', (data) => {
console.log('User login:', data);
});
nsEmitter.emit('user:login', {id: 123});
// Triggers both 'user' and 'user:login' handlers
Key Points: Observer pattern enables one-to-many communication.
Subject maintains observers list, notifies on changes. Event emitter with
on/off/emit methods.
once() for one-time listeners. Return unsubscribe function from on(). Useful for
state management, UI components, pub/sub systems. Loose coupling between components.
22.3 Factory and Builder Patterns
Factory Pattern Types
| Type | Description | Use Case |
|---|---|---|
| Simple Factory | Single factory function | Basic object creation |
| Factory Method | Subclasses decide type | Polymorphic creation |
| Abstract Factory | Family of related objects | Platform-specific objects |
| Static Factory | Static method creates instances | Named constructors |
Builder Pattern Benefits
| Benefit | Description | When to Use |
|---|---|---|
| Fluent Interface | Chainable method calls | Readable construction code |
| Immutability | Build once, immutable result | Thread-safe objects |
| Complex Construction | Step-by-step building | Objects with many parameters |
| Validation | Validate before creation | Ensure valid state |
Factory vs Constructor
| Aspect | Factory | Constructor |
|---|---|---|
| Return type | Can return any type | Always returns instance |
| Naming | Descriptive names | Class name only |
| Caching | Can return cached instances | Always creates new |
| Polymorphism | Return different subtypes | Fixed type |
Example: Factory patterns
// Simple Factory
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
getPermissions() {
return [];
}
}
class Admin extends User {
getPermissions() {
return ['read', 'write', 'delete'];
}
}
class Guest extends User {
getPermissions() {
return ['read'];
}
}
// Factory function
function createUser(name, role) {
switch (role) {
case 'admin':
return new Admin(name, role);
case 'guest':
return new Guest(name, role);
default:
return new User(name, role);
}
}
// Usage
const admin = createUser('John', 'admin');
console.log(admin.getPermissions()); // ['read', 'write', 'delete']
const guest = createUser('Jane', 'guest');
console.log(guest.getPermissions()); // ['read']
// Factory Method Pattern
class Document {
constructor(title) {
this.title = title;
}
render() {
throw new Error('Must implement render()');
}
}
class PDFDocument extends Document {
render() {
return `PDF: ${this.title}`;
}
}
class WordDocument extends Document {
render() {
return `Word: ${this.title}`;
}
}
class DocumentFactory {
createDocument(type, title) {
switch (type) {
case 'pdf':
return new PDFDocument(title);
case 'word':
return new WordDocument(title);
default:
throw new Error('Unknown document type');
}
}
}
// Usage
const factory = new DocumentFactory();
const pdf = factory.createDocument('pdf', 'Report');
console.log(pdf.render());
// Static Factory Methods
class Color {
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
static fromRGB(r, g, b) {
return new Color(r, g, b);
}
static fromHex(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
static red() {
return new Color(255, 0, 0);
}
static green() {
return new Color(0, 255, 0);
}
static blue() {
return new Color(0, 0, 255);
}
}
// Usage
const color1 = Color.fromRGB(255, 128, 0);
const color2 = Color.fromHex('#FF8000');
const red = Color.red();
// Object Pool Factory
class ObjectPool {
constructor(factory, maxSize = 10) {
this.factory = factory;
this.maxSize = maxSize;
this.available = [];
this.inUse = [];
}
acquire() {
let obj;
if (this.available.length > 0) {
obj = this.available.pop();
} else if (this.inUse.length < this.maxSize) {
obj = this.factory();
} else {
throw new Error('Pool exhausted');
}
this.inUse.push(obj);
return obj;
}
release(obj) {
const index = this.inUse.indexOf(obj);
if (index > -1) {
this.inUse.splice(index, 1);
this.available.push(obj);
}
}
}
// Usage
const connectionPool = new ObjectPool(
() => ({id: Math.random(), connected: true}),
5
);
const conn1 = connectionPool.acquire();
const conn2 = connectionPool.acquire();
connectionPool.release(conn1);
// Abstract Factory Pattern
class Button {
render() {
throw new Error('Must implement');
}
}
class WindowsButton extends Button {
render() {
return '<button class="windows">Click</button>';
}
}
class MacButton extends Button {
render() {
return '<button class="mac">Click</button>';
}
}
class Checkbox {
render() {
throw new Error('Must implement');
}
}
class WindowsCheckbox extends Checkbox {
render() {
return '<input type="checkbox" class="windows">';
}
}
class MacCheckbox extends Checkbox {
render() {
return '<input type="checkbox" class="mac">';
}
}
// Abstract Factory
class UIFactory {
createButton() {
throw new Error('Must implement');
}
createCheckbox() {
throw new Error('Must implement');
}
}
class WindowsFactory extends UIFactory {
createButton() {
return new WindowsButton();
}
createCheckbox() {
return new WindowsCheckbox();
}
}
class MacFactory extends UIFactory {
createButton() {
return new MacButton();
}
createCheckbox() {
return new MacCheckbox();
}
}
// Usage
function createUI(factory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
return {
button: button.render(),
checkbox: checkbox.render()
};
}
const windowsFactory = new WindowsFactory();
const windowsUI = createUI(windowsFactory);
const macFactory = new MacFactory();
const macUI = createUI(macFactory);
Example: Builder pattern
// Builder Pattern
class User2 {
constructor(builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
}
class UserBuilder {
constructor(name) {
this.name = name;
}
withEmail(email) {
this.email = email;
return this; // Return this for chaining
}
withAge(age) {
this.age = age;
return this;
}
withAddress(address) {
this.address = address;
return this;
}
withPhone(phone) {
this.phone = phone;
return this;
}
build() {
// Validation
if (!this.email) {
throw new Error('Email is required');
}
return new User2(this);
}
}
// Usage (fluent interface)
const user = new UserBuilder('John Doe')
.withEmail('john@example.com')
.withAge(30)
.withAddress('123 Main St')
.withPhone('555-1234')
.build();
console.log(user);
// Query Builder
class QueryBuilder {
constructor() {
this.query = {
table: null,
fields: ['*'],
conditions: [],
orderBy: null,
limit: null
};
}
select(...fields) {
this.query.fields = fields;
return this;
}
from(table) {
this.query.table = table;
return this;
}
where(condition) {
this.query.conditions.push(condition);
return this;
}
orderBy(field, direction = 'ASC') {
this.query.orderBy = {field, direction};
return this;
}
limit(count) {
this.query.limit = count;
return this;
}
build() {
let sql = `SELECT ${this.query.fields.join(', ')}`;
sql += ` FROM ${this.query.table}`;
if (this.query.conditions.length > 0) {
sql += ` WHERE ${this.query.conditions.join(' AND ')}`;
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy.field} ${this.query.orderBy.direction}`;
}
if (this.query.limit) {
sql += ` LIMIT ${this.query.limit}`;
}
return sql;
}
}
// Usage
const query = new QueryBuilder()
.select('id', 'name', 'email')
.from('users')
.where('age > 18')
.where('status = "active"')
.orderBy('name', 'ASC')
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND status = "active" ORDER BY name ASC LIMIT 10
// HTML Builder
class HTMLBuilder {
constructor(tag) {
this.element = {
tag: tag,
attributes: {},
children: [],
text: null
};
}
attr(name, value) {
this.element.attributes[name] = value;
return this;
}
text(content) {
this.element.text = content;
return this;
}
child(builder) {
if (builder instanceof HTMLBuilder) {
this.element.children.push(builder.element);
} else {
this.element.children.push(builder);
}
return this;
}
build() {
let html = `<${this.element.tag}`;
// Add attributes
for (const [key, value] of Object.entries(this.element.attributes)) {
html += ` ${key}="${value}"`;
}
html += '>';
// Add text or children
if (this.element.text) {
html += this.element.text;
} else {
this.element.children.forEach(child => {
if (typeof child === 'string') {
html += child;
} else {
html += new HTMLBuilder(child.tag)
.attr(Object.keys(child.attributes)[0], Object.values(child.attributes)[0])
.text(child.text)
.build();
}
});
}
html += `</${this.element.tag}>`;
return html;
}
}
// Usage
const html = new HTMLBuilder('div')
.attr('class', 'container')
.child(
new HTMLBuilder('h1')
.attr('class', 'title')
.text('Hello World')
)
.child(
new HTMLBuilder('p')
.text('This is a paragraph')
)
.build();
console.log(html);
// <div class="container"><h1 class="title">Hello World</h1><p>This is a paragraph</p></div>
Key Points: Factory pattern encapsulates object creation logic.
Use static factory methods for named constructors. Abstract factory creates families of related objects.
Builder pattern uses fluent interface for complex construction. Builder validates
before creating object. Good for objects with many optional parameters. Both patterns improve code
maintainability.
22.4 Singleton and Registry Patterns
Singleton Characteristics
| Characteristic | Description | Implementation |
|---|---|---|
| Single Instance | Only one instance exists | Private constructor or closure |
| Global Access | Accessible from anywhere | Static getInstance() method |
| Lazy Initialization | Created only when needed | Check if instance exists |
| Thread Safety | Safe in multi-threaded env | Synchronization (JS single-threaded) |
Registry Pattern Uses
| Use Case | Purpose | Example |
|---|---|---|
| Service Locator | Central service registry | DI container |
| Plugin System | Register/retrieve plugins | Extension manager |
| Factory Registry | Register factory functions | Component registry |
| Event Registry | Central event handlers | Event bus |
Singleton vs Global Variable
| Aspect | Singleton | Global Variable |
|---|---|---|
| Instantiation | Lazy, controlled | Immediate |
| Encapsulation | Private members possible | All public |
| Inheritance | Can extend classes | Cannot extend |
| Testability | Can be mocked | Hard to test |
Example: Singleton pattern
// Classic Singleton (ES6 class)
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
this.connected = false;
Database.instance = this;
}
connect(connectionString) {
if (!this.connected) {
this.connection = connectionString;
this.connected = true;
console.log('Database connected');
}
}
query(sql) {
if (!this.connected) {
throw new Error('Not connected');
}
console.log(`Executing: ${sql}`);
return [];
}
disconnect() {
if (this.connected) {
this.connection = null;
this.connected = false;
console.log('Database disconnected');
}
}
}
// Usage
const db1 = new Database();
db1.connect('mongodb://localhost');
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
// Module Singleton (Using ES6 modules)
// logger.js
class Logger {
constructor() {
this.logs = [];
}
log(message) {
const entry = {
message,
timestamp: new Date(),
level: 'info'
};
this.logs.push(entry);
console.log(`[${entry.timestamp.toISOString()}] ${message}`);
}
error(message) {
const entry = {
message,
timestamp: new Date(),
level: 'error'
};
this.logs.push(entry);
console.error(`[${entry.timestamp.toISOString()}] ERROR: ${message}`);
}
getLogs() {
return this.logs;
}
clear() {
this.logs = [];
}
}
// Export singleton instance
export default new Logger();
// Usage in other files
// import logger from './logger.js';
// logger.log('Application started');
// Singleton with getInstance()
class Config {
static instance = null;
constructor() {
if (Config.instance) {
throw new Error('Use Config.getInstance()');
}
this.settings = {};
}
static getInstance() {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
set(key, value) {
this.settings[key] = value;
}
get(key) {
return this.settings[key];
}
getAll() {
return {...this.settings};
}
}
// Usage
const config1 = Config.getInstance();
config1.set('apiUrl', 'https://api.example.com');
const config2 = Config.getInstance();
console.log(config2.get('apiUrl')); // https://api.example.com
console.log(config1 === config2); // true
// Closure-based Singleton
const AppState = (function() {
let instance;
function createInstance() {
const state = {
user: null,
settings: {},
cache: new Map()
};
return {
getState() {
return state;
},
setUser(user) {
state.user = user;
},
getUser() {
return state.user;
},
setSetting(key, value) {
state.settings[key] = value;
},
getSetting(key) {
return state.settings[key];
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Usage
const state1 = AppState.getInstance();
state1.setUser({name: 'John', id: 123});
const state2 = AppState.getInstance();
console.log(state2.getUser()); // {name: 'John', id: 123}
// Singleton with initialization options
class Cache {
static instances = new Map();
constructor(name, options = {}) {
const key = name;
if (Cache.instances.has(key)) {
return Cache.instances.get(key);
}
this.name = name;
this.maxSize = options.maxSize || 100;
this.ttl = options.ttl || 60000; // 1 minute
this.store = new Map();
Cache.instances.set(key, this);
}
set(key, value) {
if (this.store.size >= this.maxSize) {
const firstKey = this.store.keys().next().value;
this.store.delete(firstKey);
}
this.store.set(key, {
value,
expires: Date.now() + this.ttl
});
}
get(key) {
const item = this.store.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.store.delete(key);
return null;
}
return item.value;
}
clear() {
this.store.clear();
}
}
// Usage - named singletons
const userCache = new Cache('users', {maxSize: 50});
const productCache = new Cache('products', {maxSize: 100});
userCache.set('user:1', {name: 'John'});
const userCache2 = new Cache('users');
console.log(userCache2.get('user:1')); // {name: 'John'}
console.log(userCache === userCache2); // true
Example: Registry pattern
// Service Registry / Dependency Injection Container
class ServiceRegistry {
constructor() {
this.services = new Map();
this.singletons = new Map();
}
register(name, factory, options = {}) {
this.services.set(name, {
factory,
singleton: options.singleton || false,
dependencies: options.dependencies || []
});
}
resolve(name) {
const service = this.services.get(name);
if (!service) {
throw new Error(`Service '${name}' not registered`);
}
// Return singleton if already created
if (service.singleton && this.singletons.has(name)) {
return this.singletons.get(name);
}
// Resolve dependencies
const dependencies = service.dependencies.map(dep => this.resolve(dep));
// Create instance
const instance = service.factory(...dependencies);
// Store singleton
if (service.singleton) {
this.singletons.set(name, instance);
}
return instance;
}
has(name) {
return this.services.has(name);
}
clear() {
this.services.clear();
this.singletons.clear();
}
}
// Usage
const registry = new ServiceRegistry();
// Register services
registry.register('database', () => {
return {
query: (sql) => console.log('Query:', sql)
};
}, {singleton: true});
registry.register('userService', (db) => {
return {
getUser: (id) => {
db.query(`SELECT * FROM users WHERE id = ${id}`);
return {id, name: 'John'};
}
};
}, {dependencies: ['database']});
// Resolve services
const userService = registry.resolve('userService');
userService.getUser(123);
// Plugin Registry
class PluginRegistry {
constructor() {
this.plugins = new Map();
this.hooks = new Map();
}
register(name, plugin) {
if (this.plugins.has(name)) {
throw new Error(`Plugin '${name}' already registered`);
}
this.plugins.set(name, plugin);
// Initialize plugin
if (typeof plugin.init === 'function') {
plugin.init(this);
}
return this;
}
get(name) {
return this.plugins.get(name);
}
has(name) {
return this.plugins.has(name);
}
unregister(name) {
const plugin = this.plugins.get(name);
if (plugin && typeof plugin.destroy === 'function') {
plugin.destroy();
}
return this.plugins.delete(name);
}
// Hook system
registerHook(hookName, callback) {
if (!this.hooks.has(hookName)) {
this.hooks.set(hookName, []);
}
this.hooks.get(hookName).push(callback);
}
executeHook(hookName, data) {
const callbacks = this.hooks.get(hookName) || [];
let result = data;
for (const callback of callbacks) {
result = callback(result);
}
return result;
}
}
// Usage
const plugins = new PluginRegistry();
// Register plugins
plugins.register('markdown', {
init(registry) {
console.log('Markdown plugin initialized');
registry.registerHook('beforeRender', (text) => {
return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
});
},
render(text) {
return text;
},
destroy() {
console.log('Markdown plugin destroyed');
}
});
plugins.register('emoji', {
init(registry) {
registry.registerHook('beforeRender', (text) => {
return text.replace(/:smile:/g, '😊');
});
}
});
// Execute hooks
let content = 'Hello **world** :smile:';
content = plugins.executeHook('beforeRender', content);
console.log(content); // Hello <strong>world</strong> 😊
// Component Registry
class ComponentRegistry {
constructor() {
this.components = new Map();
}
register(name, component) {
this.components.set(name, component);
return this;
}
create(name, props = {}) {
const Component = this.components.get(name);
if (!Component) {
throw new Error(`Component '${name}' not registered`);
}
if (typeof Component === 'function') {
return new Component(props);
}
return Component;
}
getAll() {
return Array.from(this.components.keys());
}
}
// Usage
const components = new ComponentRegistry();
components.register('Button', class {
constructor(props) {
this.label = props.label || 'Click';
}
render() {
return `<button>${this.label}</button>`;
}
});
components.register('Input', class {
constructor(props) {
this.type = props.type || 'text';
this.placeholder = props.placeholder || '';
}
render() {
return `<input type="${this.type}" placeholder="${this.placeholder}">`;
}
});
const button = components.create('Button', {label: 'Submit'});
console.log(button.render());
const input = components.create('Input', {placeholder: 'Enter name'});
console.log(input.render());
console.log('Registered components:', components.getAll());
Key Points: Singleton pattern ensures single instance with
global access. Use constructor check or
getInstance() method. ES6 module exports create natural
singletons. Registry pattern manages collections of objects. Service registry
for dependency injection. Plugin registry for extensible systems. Component registry for dynamic creation.
22.5 Strategy and Command Patterns
Strategy Pattern Components
| Component | Role | Responsibility |
|---|---|---|
| Context | Uses strategy | Maintains reference to strategy |
| Strategy Interface | Common interface | Defines algorithm method |
| Concrete Strategy | Implements algorithm | Specific implementation |
Command Pattern Elements
| Element | Purpose | Implementation |
|---|---|---|
| Command | Encapsulates action | execute() method |
| Receiver | Performs action | Business logic |
| Invoker | Triggers command | Calls execute() |
| Client | Creates command | Binds receiver to command |
Pattern Benefits
| Pattern | Key Benefit | Use Case |
|---|---|---|
| Strategy | Runtime algorithm selection | Sorting, validation, payment methods |
| Command | Undo/redo, queuing, logging | Text editors, transactions, macros |
Example: Strategy pattern
// Strategy Pattern - Sorting algorithms
class SortStrategy {
sort(array) {
throw new Error('Must implement sort()');
}
}
class BubbleSortStrategy extends SortStrategy {
sort(array) {
const arr = [...array];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSortStrategy extends SortStrategy {
sort(array) {
if (array.length <= 1) return array;
const pivot = array[Math.floor(array.length / 2)];
const left = array.filter(x => x < pivot);
const middle = array.filter(x => x === pivot);
const right = array.filter(x => x > pivot);
return [...this.sort(left), ...middle, ...this.sort(right)];
}
}
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(array) {
console.log(`Sorting with ${this.strategy.constructor.name}`);
return this.strategy.sort(array);
}
}
// Usage
const data = [5, 2, 8, 1, 9];
const sorter = new Sorter(new BubbleSortStrategy());
console.log(sorter.sort(data));
sorter.setStrategy(new QuickSortStrategy());
console.log(sorter.sort(data));
// Strategy Pattern - Validation
class ValidationStrategy {
validate(value) {
throw new Error('Must implement validate()');
}
}
class EmailValidation extends ValidationStrategy {
validate(value) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
}
}
class PhoneValidation extends ValidationStrategy {
validate(value) {
const regex = /^\d{3}-\d{3}-\d{4}$/;
return regex.test(value);
}
}
class PasswordValidation extends ValidationStrategy {
validate(value) {
return value.length >= 8 &&
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/[0-9]/.test(value);
}
}
class Validator {
constructor() {
this.strategies = new Map();
}
addStrategy(field, strategy) {
this.strategies.set(field, strategy);
}
validate(data) {
const errors = {};
for (const [field, strategy] of this.strategies) {
const value = data[field];
if (!strategy.validate(value)) {
errors[field] = `Invalid ${field}`;
}
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
}
// Usage
const validator = new Validator();
validator.addStrategy('email', new EmailValidation());
validator.addStrategy('phone', new PhoneValidation());
validator.addStrategy('password', new PasswordValidation());
const result = validator.validate({
email: 'user@example.com',
phone: '555-123-4567',
password: 'Pass123'
});
console.log(result); // {valid: true, errors: {}}
// Strategy Pattern - Payment methods
class PaymentStrategy {
pay(amount) {
throw new Error('Must implement pay()');
}
}
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber) {
super();
this.cardNumber = cardNumber;
}
pay(amount) {
console.log(`Paid ${amount} with credit card ${this.cardNumber}`);
return {success: true, transactionId: Math.random()};
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Paid ${amount} via PayPal (${this.email})`);
return {success: true, transactionId: Math.random()};
}
}
class CryptoPayment extends PaymentStrategy {
constructor(wallet) {
super();
this.wallet = wallet;
}
pay(amount) {
console.log(`Paid ${amount} with crypto (${this.wallet})`);
return {success: true, transactionId: Math.random()};
}
}
class ShoppingCart {
constructor() {
this.items = [];
this.paymentStrategy = null;
}
addItem(item) {
this.items.push(item);
}
setPaymentStrategy(strategy) {
this.paymentStrategy = strategy;
}
checkout() {
const total = this.items.reduce((sum, item) => sum + item.price, 0);
if (!this.paymentStrategy) {
throw new Error('Payment method not set');
}
return this.paymentStrategy.pay(total);
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem({name: 'Book', price: 20});
cart.addItem({name: 'Pen', price: 5});
cart.setPaymentStrategy(new CreditCardPayment('1234-5678-9012-3456'));
cart.checkout();
cart.setPaymentStrategy(new PayPalPayment('user@example.com'));
cart.checkout();
Example: Command pattern
// Command Pattern - Text editor
class Command {
execute() {
throw new Error('Must implement execute()');
}
undo() {
throw new Error('Must implement undo()');
}
}
class InsertTextCommand extends Command {
constructor(receiver, text, position) {
super();
this.receiver = receiver;
this.text = text;
this.position = position;
}
execute() {
this.receiver.insertText(this.text, this.position);
}
undo() {
this.receiver.deleteText(this.position, this.text.length);
}
}
class DeleteTextCommand extends Command {
constructor(receiver, position, length) {
super();
this.receiver = receiver;
this.position = position;
this.length = length;
this.deletedText = null;
}
execute() {
this.deletedText = this.receiver.deleteText(this.position, this.length);
}
undo() {
this.receiver.insertText(this.deletedText, this.position);
}
}
// Receiver
class TextEditor {
constructor() {
this.content = '';
}
insertText(text, position) {
this.content =
this.content.slice(0, position) +
text +
this.content.slice(position);
console.log('Content:', this.content);
}
deleteText(position, length) {
const deleted = this.content.slice(position, position + length);
this.content =
this.content.slice(0, position) +
this.content.slice(position + length);
console.log('Content:', this.content);
return deleted;
}
getText() {
return this.content;
}
}
// Invoker
class CommandManager {
constructor() {
this.history = [];
this.currentIndex = -1;
}
execute(command) {
// Remove any commands after current index
this.history = this.history.slice(0, this.currentIndex + 1);
command.execute();
this.history.push(command);
this.currentIndex++;
}
undo() {
if (this.currentIndex < 0) {
console.log('Nothing to undo');
return;
}
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
}
redo() {
if (this.currentIndex >= this.history.length - 1) {
console.log('Nothing to redo');
return;
}
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
}
}
// Usage
const editor = new TextEditor();
const manager = new CommandManager();
manager.execute(new InsertTextCommand(editor, 'Hello', 0));
manager.execute(new InsertTextCommand(editor, ' World', 5));
manager.execute(new DeleteTextCommand(editor, 5, 6));
manager.undo(); // Restore " World"
manager.undo(); // Remove " World"
manager.redo(); // Add " World" again
// Command Pattern - Remote control
class Light {
constructor(location) {
this.location = location;
this.isOn = false;
}
on() {
this.isOn = true;
console.log(`${this.location} light is ON`);
}
off() {
this.isOn = false;
console.log(`${this.location} light is OFF`);
}
}
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.on();
}
undo() {
this.light.off();
}
}
class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.off();
}
undo() {
this.light.on();
}
}
class RemoteControl {
constructor() {
this.commands = {};
this.history = [];
}
setCommand(button, command) {
this.commands[button] = command;
}
pressButton(button) {
const command = this.commands[button];
if (command) {
command.execute();
this.history.push(command);
}
}
pressUndo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// Usage
const livingRoomLight = new Light('Living Room');
const bedroomLight = new Light('Bedroom');
const remote = new RemoteControl();
remote.setCommand('button1', new LightOnCommand(livingRoomLight));
remote.setCommand('button2', new LightOffCommand(livingRoomLight));
remote.setCommand('button3', new LightOnCommand(bedroomLight));
remote.pressButton('button1'); // Living Room light ON
remote.pressButton('button3'); // Bedroom light ON
remote.pressUndo(); // Bedroom light OFF
// Macro Command - multiple commands
class MacroCommand extends Command {
constructor(commands) {
super();
this.commands = commands;
}
execute() {
this.commands.forEach(cmd => cmd.execute());
}
undo() {
// Undo in reverse order
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo();
}
}
}
// Usage
const partyMode = new MacroCommand([
new LightOnCommand(livingRoomLight),
new LightOnCommand(bedroomLight)
]);
remote.setCommand('party', partyMode);
remote.pressButton('party'); // Turn on all lights
Key Points: Strategy pattern encapsulates algorithms as
interchangeable objects. Set strategy at runtime. Use for sorting, validation, payment methods.
Command pattern encapsulates actions as objects. Enables undo/redo with
command history. Macro commands combine multiple commands. Both patterns promote loose coupling.
22.6 Decorator and Proxy Patterns
Decorator Pattern Features
| Feature | Description | Benefit |
|---|---|---|
| Wrapping | Wrap object to add behavior | Extend without modifying |
| Composition | Stack multiple decorators | Flexible combinations |
| Transparent | Same interface as original | Drop-in replacement |
| Runtime | Add behavior dynamically | Configuration flexibility |
Proxy Pattern Types
| Type | Purpose | Use Case |
|---|---|---|
| Virtual Proxy | Lazy initialization | Expensive object creation |
| Protection Proxy | Access control | Authentication, authorization |
| Remote Proxy | Remote object access | API calls, RPC |
| Caching Proxy | Cache results | Performance optimization |
| Logging Proxy | Log method calls | Debugging, auditing |
ES6 Proxy Traps
| Trap | Intercepts | Usage |
|---|---|---|
| get | Property access | obj.prop |
| set | Property assignment | obj.prop = value |
| has | in operator | 'prop' in obj |
| deleteProperty | delete operator | delete obj.prop |
| apply | Function call | func(...args) |
| construct | new operator | new Constructor() |
Example: Decorator pattern
// Decorator Pattern - Coffee shop
class Coffee {
cost() {
return 5;
}
description() {
return 'Coffee';
}
}
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', Milk';
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return this.coffee.description() + ', Sugar';
}
}
class WhipDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1.5;
}
description() {
return this.coffee.description() + ', Whip';
}
}
// Usage
let myCoffee = new Coffee();
console.log(`${myCoffee.description()}: ${myCoffee.cost()}`);
myCoffee = new MilkDecorator(myCoffee);
console.log(`${myCoffee.description()}: ${myCoffee.cost()}`);
myCoffee = new SugarDecorator(myCoffee);
myCoffee = new WhipDecorator(myCoffee);
console.log(`${myCoffee.description()}: ${myCoffee.cost()}`);
// Coffee, Milk, Sugar, Whip: $8
// Function Decorator
function logger(func) {
return function(...args) {
console.log(`Calling ${func.name} with`, args);
const result = func(...args);
console.log(`Result:`, result);
return result;
};
}
function memoize(func) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit');
return cache.get(key);
}
const result = func(...args);
cache.set(key, result);
return result;
};
}
function timer(func) {
return function(...args) {
const start = performance.now();
const result = func(...args);
const end = performance.now();
console.log(`${func.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Usage
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const decoratedFib = timer(memoize(logger(fibonacci)));
decoratedFib(10);
// Class Method Decorator (using decorator proposal)
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
function validate(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (args.some(arg => typeof arg !== 'number')) {
throw new Error('All arguments must be numbers');
}
return original.apply(this, args);
};
return descriptor;
}
class Calculator {
@readonly
PI = 3.14159;
@validate
add(a, b) {
return a + b;
}
}
// Without decorator syntax (current JavaScript)
class Calculator2 {
constructor() {
Object.defineProperty(this, 'PI', {
value: 3.14159,
writable: false
});
}
}
// Wrap with validation
Calculator2.prototype.add = function(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('All arguments must be numbers');
}
return a + b;
};
Example: Proxy pattern
// Virtual Proxy - Lazy loading
class Image {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading ${this.filename} from disk`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
class ImageProxy {
constructor(filename) {
this.filename = filename;
this.image = null;
}
display() {
if (!this.image) {
this.image = new Image(this.filename);
}
this.image.display();
}
}
// Usage
const proxy = new ImageProxy('photo.jpg');
// Image not loaded yet
proxy.display(); // Loads and displays
proxy.display(); // Just displays (already loaded)
// Protection Proxy - Access control
class BankAccount {
constructor(balance) {
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
console.log(`Deposited ${amount}, balance: ${this.balance}`);
}
withdraw(amount) {
if (amount > this.balance) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
console.log(`Withdrew ${amount}, balance: ${this.balance}`);
}
getBalance() {
return this.balance;
}
}
class BankAccountProxy {
constructor(account, password) {
this.account = account;
this.password = password;
}
authenticate(password) {
return password === this.password;
}
deposit(amount, password) {
if (!this.authenticate(password)) {
throw new Error('Authentication failed');
}
this.account.deposit(amount);
}
withdraw(amount, password) {
if (!this.authenticate(password)) {
throw new Error('Authentication failed');
}
this.account.withdraw(amount);
}
getBalance(password) {
if (!this.authenticate(password)) {
throw new Error('Authentication failed');
}
return this.account.getBalance();
}
}
// Usage
const account = new BankAccount(1000);
const secureAccount = new BankAccountProxy(account, 'secret123');
secureAccount.deposit(500, 'secret123');
secureAccount.withdraw(200, 'secret123');
// secureAccount.withdraw(200, 'wrong'); // Error
// ES6 Proxy - Validation
const user = {
name: 'John',
age: 30,
email: 'john@example.com'
};
const validatedUser = new Proxy(user, {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || value < 0) {
throw new Error('Age must be a positive number');
}
}
if (property === 'email') {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error('Invalid email format');
}
}
target[property] = value;
return true;
}
});
validatedUser.age = 35; // OK
// validatedUser.age = -5; // Error
// validatedUser.email = 'invalid'; // Error
// ES6 Proxy - Logging
function createLoggingProxy(obj, name) {
return new Proxy(obj, {
get(target, property) {
console.log(`[GET] ${name}.${property}`);
return target[property];
},
set(target, property, value) {
console.log(`[SET] ${name}.${property} = ${value}`);
target[property] = value;
return true;
}
});
}
const person = createLoggingProxy({
name: 'John',
age: 30
}, 'person');
person.name; // [GET] person.name
person.age = 31; // [SET] person.age = 31
// ES6 Proxy - Negative array indexes
function createArrayProxy(arr) {
return new Proxy(arr, {
get(target, property) {
const index = parseInt(property);
if (!isNaN(index) && index < 0) {
return target[target.length + index];
}
return target[property];
}
});
}
const arr = createArrayProxy([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
// ES6 Proxy - Observable
function createObservable(obj, onChange) {
return new Proxy(obj, {
set(target, property, value) {
const oldValue = target[property];
target[property] = value;
onChange({
property,
oldValue,
newValue: value
});
return true;
}
});
}
const state = createObservable({count: 0}, (change) => {
console.log(`${change.property} changed from ${change.oldValue} to ${change.newValue}`);
});
state.count = 1; // count changed from 0 to 1
state.count = 2; // count changed from 1 to 2
// Caching Proxy
function createCachingProxy(target) {
const cache = new Map();
return new Proxy(target, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit');
return cache.get(key);
}
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
}
function expensiveOperation(n) {
console.log('Computing...');
let result = 0;
for (let i = 0; i < n * 1000000; i++) {
result += i;
}
return result;
}
const cachedOp = createCachingProxy(expensiveOperation);
cachedOp(10); // Computing...
cachedOp(10); // Cache hit (instant)
cachedOp(20); // Computing...
Key Points: Decorator pattern wraps objects to add behavior
dynamically. Stack multiple decorators for flexible combinations. Proxy pattern
controls access to objects. Virtual proxy for lazy loading. Protection proxy for access control. ES6 Proxy
with traps (
get, set, apply) enables powerful interceptors. Use for
validation, logging, caching, observables.
Section 22 Summary: Modern JavaScript Patterns and Techniques
- Module Patterns: IIFE modules, revealing module, ES6 modules (import/export), namespace objects for organization
- Observer Pattern: Subject maintains observers, notify on changes, Event Emitter with on/off/emit methods
- Factory Pattern: Encapsulate object creation, static factory methods, abstract factory for related objects
- Builder Pattern: Fluent interface for complex construction, chainable methods, validation before creation
- Singleton: Single instance with global access, getInstance() method, ES6 module exports as natural singletons
- Registry Pattern: Manage collections of objects, service registry (DI), plugin registry, component registry
- Strategy Pattern: Encapsulate algorithms, runtime selection, interchangeable strategies for sorting/validation/payments
- Command Pattern: Encapsulate actions as objects, undo/redo with command history, macro commands for multiple actions
- Decorator Pattern: Wrap objects to add behavior, stack decorators, same interface as original object
- Proxy Pattern: Control object access, types (virtual, protection, caching), ES6 Proxy traps (get/set/apply)
- Pattern Benefits: All patterns promote loose coupling, code reusability, maintainability, testability
- Use Cases: Choose pattern based on problem: Observer for events, Factory for creation, Strategy for algorithms, Command for undo/redo, Proxy for access control
23. Functional Programming Concepts
23.1 Pure Functions and Side Effects Management
Pure Function Characteristics
| Characteristic | Description | Benefit |
|---|---|---|
| Deterministic | Same input → same output | Predictable, testable |
| No Side Effects | Doesn't modify external state | Safer, easier to reason about |
| No External Dependencies | Only uses parameters | Self-contained, portable |
| Referential Transparency | Can replace call with result | Optimizable, cacheable |
Common Side Effects
| Side Effect | Example | Impact |
|---|---|---|
| Mutation | Modifying objects/arrays | Unpredictable state changes |
| I/O Operations | Console.log, file read/write | Non-deterministic behavior |
| DOM Manipulation | Changing HTML elements | External state modification |
| Network Requests | API calls, fetch | Async, unpredictable timing |
| Global State | Accessing/modifying globals | Hidden dependencies |
| Random/Date | Math.random(), Date.now() | Non-deterministic output |
Managing Side Effects
| Strategy | Technique | Usage |
|---|---|---|
| Isolation | Keep side effects at boundaries | Pure core, impure shell |
| Dependency Injection | Pass dependencies as parameters | Explicit dependencies |
| Immutability | Never modify, create new | Prevent mutation side effects |
| Functional Core | Pure logic separate from I/O | Testable business logic |
Example: Pure functions vs impure
// IMPURE: Modifies external state
let total = 0;
function addToTotal(value) {
total += value; // Side effect: modifies global
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 (different output!)
// PURE: No side effects
function add(a, b) {
return a + b;
}
console.log(add(5, 5)); // 10
console.log(add(5, 5)); // 10 (always same output)
// IMPURE: Mutates input
function addItemImpure(array, item) {
array.push(item); // Mutates original array
return array;
}
const arr1 = [1, 2, 3];
addItemImpure(arr1, 4);
console.log(arr1); // [1, 2, 3, 4] - original modified!
// PURE: Returns new array
function addItemPure(array, item) {
return [...array, item]; // Creates new array
}
const arr2 = [1, 2, 3];
const arr3 = addItemPure(arr2, 4);
console.log(arr2); // [1, 2, 3] - original unchanged
console.log(arr3); // [1, 2, 3, 4] - new array
// IMPURE: Relies on external state
let discount = 0.1;
function calculatePriceImpure(price) {
return price * (1 - discount); // Uses external variable
}
// PURE: All dependencies explicit
function calculatePricePure(price, discount) {
return price * (1 - discount); // All inputs are parameters
}
console.log(calculatePricePure(100, 0.1)); // 90
// IMPURE: I/O operations
function logAndDouble(x) {
console.log(`Value: ${x}`); // Side effect: console output
return x * 2;
}
// PURE: Separate logic from I/O
function double(x) {
return x * 2;
}
function logValue(x) {
console.log(`Value: ${x}`);
}
// Use together
const result = double(5);
logValue(result);
// IMPURE: Non-deterministic
function getCurrentTimestamp() {
return Date.now(); // Different each call
}
// PURE: Pass time as parameter
function addHours(timestamp, hours) {
return timestamp + (hours * 60 * 60 * 1000);
}
// Managing side effects pattern
class UserService {
// Impure methods at boundaries
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Call pure function for transformation
return this.transformUserData(data);
}
// Pure function - easy to test
transformUserData(data) {
return {
id: data.id,
name: data.name.toUpperCase(),
age: data.age,
isAdult: data.age >= 18
};
}
}
// Pure core, impure shell pattern
// Pure core - business logic
const calculateOrderTotal = (items, taxRate) => {
const subtotal = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const tax = subtotal * taxRate;
return {
subtotal,
tax,
total: subtotal + tax
};
};
const validateOrder = (order) => {
if (!order.items || order.items.length === 0) {
return {valid: false, error: 'No items in order'};
}
if (order.items.some(item => item.price < 0)) {
return {valid: false, error: 'Invalid price'};
}
return {valid: true};
};
// Impure shell - handles I/O
const processOrder = async (orderId) => {
// Side effect: fetch from API
const order = await fetch(`/api/orders/${orderId}`).then(r => r.json());
// Pure: validate
const validation = validateOrder(order);
if (!validation.valid) {
throw new Error(validation.error);
}
// Pure: calculate
const total = calculateOrderTotal(order.items, 0.08);
// Side effect: save to database
await fetch(`/api/orders/${orderId}`, {
method: 'PUT',
body: JSON.stringify({...order, ...total})
});
// Side effect: send email
await sendOrderConfirmation(order.customerId, total);
return total;
};
// Dependency injection for testability
// Impure: hard to test
function getUserDataImpure(userId) {
const user = database.query(`SELECT * FROM users WHERE id = ${userId}`);
return user;
}
// Pure: inject dependencies
function getUserDataPure(userId, dbQuery) {
return dbQuery(`SELECT * FROM users WHERE id = ${userId}`);
}
// Usage
const result2 = getUserDataPure(123, database.query);
// Testing
const mockQuery = (sql) => ({id: 123, name: 'Test'});
const testResult = getUserDataPure(123, mockQuery);
// Memoization for pure functions
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Works perfectly with pure functions
const expensivePureCalc = (n) => {
console.log('Calculating...');
return n * n;
};
const memoizedCalc = memoize(expensivePureCalc);
console.log(memoizedCalc(5)); // Calculating... 25
console.log(memoizedCalc(5)); // 25 (cached, no calculation)
Key Points: Pure functions have no side effects, return same
output for same input. Side effects include mutation, I/O, DOM manipulation, network requests. Isolate side
effects at program boundaries. Use dependency injection for testability. Pure functions are cacheable with
memoization. Separate pure core (business logic) from impure shell (I/O).
23.2 Higher-Order Functions and Function Composition
Higher-Order Function Types
| Type | Description | Example |
|---|---|---|
| Takes Function | Accepts function as parameter | map, filter, reduce |
| Returns Function | Returns new function | Factory functions, currying |
| Both | Takes and returns functions | Decorators, middleware |
Common Higher-Order Functions
| Function | Purpose | Returns |
|---|---|---|
| map(fn) | Transform each element | New array |
| filter(fn) | Select elements by predicate | Filtered array |
| reduce(fn, init) | Accumulate to single value | Accumulated value |
| forEach(fn) | Execute for each element | undefined |
| find(fn) | Find first matching element | Element or undefined |
| some(fn) | Test if any match | Boolean |
| every(fn) | Test if all match | Boolean |
Function Composition Patterns
| Pattern | Direction | Usage |
|---|---|---|
| compose | Right-to-left | compose(f, g, h)(x) = f(g(h(x))) |
| pipe | Left-to-right | pipe(f, g, h)(x) = h(g(f(x))) |
| chain | Method chaining | obj.method1().method2() |
Example: Higher-order functions
// Higher-order function: takes function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Iteration ${i}`));
// Higher-order function: returns function
function greaterThan(n) {
return (m) => m > n;
}
const greaterThan10 = greaterThan(10);
console.log(greaterThan10(15)); // true
console.log(greaterThan10(5)); // false
// Array methods as higher-order functions
const numbers = [1, 2, 3, 4, 5];
// map: transform
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter: select
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
// reduce: accumulate
const sum = numbers.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15
// Complex reduce example
const users = [
{name: 'John', age: 30, country: 'USA'},
{name: 'Jane', age: 25, country: 'UK'},
{name: 'Bob', age: 30, country: 'USA'}
];
const groupedByCountry = users.reduce((acc, user) => {
if (!acc[user.country]) {
acc[user.country] = [];
}
acc[user.country].push(user);
return acc;
}, {});
console.log(groupedByCountry);
// {USA: [{John}, {Bob}], UK: [{Jane}]}
// Creating custom higher-order functions
function filter(array, predicate) {
const result = [];
for (const item of array) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
function map(array, transform) {
const result = [];
for (const item of array) {
result.push(transform(item));
}
return result;
}
// Function factory
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Function that returns function with closure
function counter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const myCounter = counter(10);
console.log(myCounter.increment()); // 11
console.log(myCounter.increment()); // 12
console.log(myCounter.value()); // 12
// Decorator pattern with higher-order functions
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
function withTiming(fn) {
return function(...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
function add2(a, b) {
return a + b;
}
const loggedAdd = withLogging(add2);
const timedAdd = withTiming(add2);
loggedAdd(2, 3);
timedAdd(2, 3);
// Stack decorators
const decoratedAdd = withLogging(withTiming(add2));
decoratedAdd(5, 10);
// Partial application helper
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
console.log(sayHello('John')); // Hello, John!
// Once function - execute only once
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return {initialized: true};
});
initialize(); // Initializing...
initialize(); // No output (already called)
Example: Function composition
// Compose: right-to-left execution
function compose(...fns) {
return function(value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
// Pipe: left-to-right execution
function pipe(...fns) {
return function(value) {
return fns.reduce((acc, fn) => fn(acc), value);
};
}
// Example functions
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// Using compose (right-to-left)
const composed = compose(square, double, addOne);
console.log(composed(3)); // square(double(addOne(3))) = square(8) = 64
// Using pipe (left-to-right)
const piped = pipe(addOne, double, square);
console.log(piped(3)); // square(double(addOne(3))) = square(8) = 64
// Real-world example: data transformation pipeline
const users2 = [
{name: 'john doe', age: 17, active: true},
{name: 'jane smith', age: 25, active: false},
{name: 'bob jones', age: 30, active: true}
];
// Individual transformation functions
const filterActive = users => users.filter(u => u.active);
const filterAdults = users => users.filter(u => u.age >= 18);
const capitalizeNames = users => users.map(u => ({
...u,
name: u.name.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}));
const extractNames = users => users.map(u => u.name);
// Compose pipeline
const processUsers = pipe(
filterActive,
filterAdults,
capitalizeNames,
extractNames
);
console.log(processUsers(users2)); // ['Bob Jones']
// Point-free style (no explicit arguments)
const isEven = x => x % 2 === 0;
const increment = x => x + 1;
// Instead of: numbers.filter(x => isEven(x)).map(x => increment(x))
// Point-free:
const processNumbers = pipe(
arr => arr.filter(isEven),
arr => arr.map(increment)
);
console.log(processNumbers([1, 2, 3, 4, 5])); // [3, 5]
// Composable validation
const validateEmail = email => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email) ? {valid: true} : {valid: false, error: 'Invalid email'};
};
const validateLength = (min, max) => str => {
return str.length >= min && str.length <= max
? {valid: true}
: {valid: false, error: `Length must be ${min}-${max}`};
};
const validatePassword = pipe(
validateLength(8, 20),
result => result.valid ?
(/[A-Z]/.test(result) ? {valid: true} : {valid: false, error: 'Need uppercase'}) :
result
);
// Composition with async functions
const asyncPipe = (...fns) => {
return async (value) => {
let result = value;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
};
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const enrichUser = async (user) => {
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
return {...user, posts};
};
const formatUser = (user) => ({
name: user.name.toUpperCase(),
postCount: user.posts.length
});
const getUserData = asyncPipe(
fetchUser,
enrichUser,
formatUser
);
// await getUserData(123);
// Transducer pattern (advanced composition)
const mapTransducer = (transform) => (reducer) => {
return (acc, value) => reducer(acc, transform(value));
};
const filterTransducer = (predicate) => (reducer) => {
return (acc, value) => predicate(value) ? reducer(acc, value) : acc;
};
const transduce = (transducer, reducer, initial, collection) => {
const xform = transducer(reducer);
return collection.reduce(xform, initial);
};
// Compose transducers
const composeTransducers = (...transducers) => {
return transducers.reduce((a, b) => (...args) => a(b(...args)));
};
// Usage
const xform = composeTransducers(
mapTransducer(x => x * 2),
filterTransducer(x => x > 5)
);
const result3 = transduce(
xform,
(acc, x) => acc.concat(x),
[],
[1, 2, 3, 4, 5]
);
console.log(result3); // [6, 8, 10]
Key Points: Higher-order functions accept functions as
arguments or return functions. Common examples: map, filter, reduce. Function
composition combines functions: compose (right-to-left), pipe (left-to-right). Decorators add behavior
by wrapping functions. Point-free style eliminates explicit arguments. Composition enables building complex
operations from simple functions.
23.3 Currying and Partial Application
Currying vs Partial Application
| Aspect | Currying | Partial Application |
|---|---|---|
| Definition | Transform f(a,b,c) to f(a)(b)(c) | Fix some arguments of function |
| Arguments | One argument at a time | Can fix multiple at once |
| Result | Always returns function | May return final result |
| Arity | Reduces to unary functions | Reduces arity by N |
Currying Benefits
| Benefit | Description | Use Case |
|---|---|---|
| Reusability | Create specialized functions | Configuration functions |
| Composition | Easier function composition | Building pipelines |
| Delayed Execution | Apply arguments over time | Event handlers, callbacks |
| Point-free Style | Write without explicit arguments | Cleaner code |
Partial Application Techniques
| Technique | Method | Example |
|---|---|---|
| bind | Function.prototype.bind | fn.bind(null, arg1, arg2) |
| Wrapper Function | Manual wrapping | (...args) => fn(fixed, ...args) |
| Helper Library | Lodash/Ramda partial | _.partial(fn, arg1) |
Example: Currying
// Manual currying
function add3(a, b, c) {
return a + b + c;
}
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// Usage
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function currying
const curriedAdd2 = a => b => c => a + b + c;
console.log(curriedAdd2(1)(2)(3)); // 6
// Generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// Usage
const add = (a, b, c) => a + b + c;
const curriedAddFunc = curry(add);
console.log(curriedAddFunc(1)(2)(3)); // 6
console.log(curriedAddFunc(1, 2)(3)); // 6
console.log(curriedAddFunc(1)(2, 3)); // 6
console.log(curriedAddFunc(1, 2, 3)); // 6
// Practical currying example: event handlers
const handleClick = curry((action, id, event) => {
event.preventDefault();
console.log(`Action: ${action}, ID: ${id}`);
});
// Create specialized handlers
const deleteHandler = handleClick('delete');
const editHandler = handleClick('edit');
// Use in event listeners
// element.addEventListener('click', deleteHandler(123));
// element.addEventListener('click', editHandler(456));
// Currying for configuration
const createLogger = level => prefix => message => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${prefix}: ${message}`);
};
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');
const appInfoLogger = infoLogger('App');
const dbInfoLogger = infoLogger('Database');
appInfoLogger('Application started');
dbInfoLogger('Connected to database');
// Currying for API calls
const fetchFromAPI = curry((baseUrl, endpoint, params) => {
const url = new URL(endpoint, baseUrl);
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
return fetch(url.toString()).then(r => r.json());
});
const fetchFromMyAPI = fetchFromAPI('https://api.example.com');
const fetchUsers = fetchFromMyAPI('/users');
const fetchPosts = fetchFromMyAPI('/posts');
// await fetchUsers({page: 1, limit: 10});
// await fetchPosts({userId: 123});
// Currying for validation
const validate = curry((regex, message, value) => {
return regex.test(value)
? {valid: true}
: {valid: false, error: message};
});
const validateEmail = validate(
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
'Invalid email format'
);
const validatePhone = validate(
/^\d{3}-\d{3}-\d{4}$/,
'Invalid phone format'
);
console.log(validateEmail('user@example.com'));
console.log(validatePhone('555-123-4567'));
// Currying with multiple arguments preserved
const multiply = (a, b) => a * b;
const curriedMultiply = curry(multiply);
const double2 = curriedMultiply(2);
const triple2 = curriedMultiply(3);
console.log(double2(5)); // 10
console.log(triple2(5)); // 15
// Auto-currying with Proxy
function autoCurry(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
if (args.length >= target.length) {
return target.apply(thisArg, args);
}
return autoCurry(target.bind(thisArg, ...args));
}
});
}
const sum = autoCurry((a, b, c, d) => a + b + c + d);
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(1)(2)(3)(4)); // 10
console.log(sum(1, 2)(3, 4)); // 10
console.log(sum(1)(2, 3)(4)); // 10
Example: Partial application
// Partial application with bind
function greet2(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello2 = greet2.bind(null, 'Hello');
console.log(sayHello2('John')); // Hello, John!
const sayHi = greet2.bind(null, 'Hi');
console.log(sayHi('Jane')); // Hi, Jane!
// Custom partial function
function partial2(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function calculate(operation, a, b) {
switch(operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
case 'divide': return a / b;
}
}
const add4 = partial2(calculate, 'add');
const subtract = partial2(calculate, 'subtract');
console.log(add4(5, 3)); // 8
console.log(subtract(10, 3)); // 7
// Partial from right (partial right)
function partialRight(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...remainingArgs, ...fixedArgs);
};
}
function divide(a, b) {
return a / b;
}
const divideBy2 = partialRight(divide, 2);
console.log(divideBy2(10)); // 5
// Placeholder support
const _ = Symbol('placeholder');
function partialWithPlaceholder(fn, ...fixedArgs) {
return function(...remainingArgs) {
const args = fixedArgs.map(arg =>
arg === _ ? remainingArgs.shift() : arg
);
return fn(...args, ...remainingArgs);
};
}
function formatDate(year, month, day) {
return `${year}-${month}-${day}`;
}
const formatWithYear = partialWithPlaceholder(formatDate, 2024, _, _);
console.log(formatWithYear(12, 25)); // 2024-12-25
const formatDecember = partialWithPlaceholder(formatDate, _, 12, _);
console.log(formatDecember(2024, 25)); // 2024-12-25
// Practical example: Array methods with partial
const numbers2 = [1, 2, 3, 4, 5];
// Create reusable predicates
const isGreaterThan = (threshold, value) => value > threshold;
const multiplyBy = (factor, value) => value * factor;
const isGreaterThan3 = partial2(isGreaterThan, 3);
const multiplyBy10 = partial2(multiplyBy, 10);
console.log(numbers2.filter(isGreaterThan3)); // [4, 5]
console.log(numbers2.map(multiplyBy10)); // [10, 20, 30, 40, 50]
// Partial for event handlers
function handleEvent(eventType, selector, handler, event) {
if (event.target.matches(selector)) {
handler(event);
}
}
const handleClick2 = partial2(handleEvent, 'click');
const handleButtonClick = partial2(handleClick2, 'button');
// Usage
// document.addEventListener('click', handleButtonClick((e) => {
// console.log('Button clicked');
// }));
// Combining curry and partial
const log = (level, module, message) => {
console.log(`[${level}] ${module}: ${message}`);
};
const curriedLog = curry(log);
// Create specialized loggers
const errorLog = curriedLog('ERROR');
const infoLog = curriedLog('INFO');
const appError = errorLog('App');
const dbInfo = infoLog('Database');
appError('Connection failed');
dbInfo('Query executed');
// Memoization with partial
function memoize2(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = (a, b, c) => {
console.log('Computing...');
return a + b + c;
};
const memoizedCalc2 = memoize2(expensiveCalc);
// Partially applied memoized function
const addTo10 = partial2(memoizedCalc2, 10);
console.log(addTo10(5, 5)); // Computing... 20
console.log(addTo10(5, 5)); // 20 (cached)
// Function application helper
const applyArgs = (...args) => fn => fn(...args);
const args = [1, 2, 3];
const result4 = applyArgs(...args)(add3);
console.log(result4); // 6
// Flip function (reverse argument order)
function flip(fn) {
return function(a, b) {
return fn(b, a);
};
}
const subtract2 = (a, b) => a - b;
const flippedSubtract = flip(subtract2);
console.log(subtract2(10, 3)); // 7
console.log(flippedSubtract(10, 3)); // -7
Key Points: Currying transforms multi-argument function to
sequence of unary functions. Partial application fixes some arguments, returns
function expecting remaining arguments. Currying enables point-free style and composition. Use
bind
or custom wrapper for partial application. Placeholder support allows flexible argument positioning. Both
techniques create specialized, reusable functions.
23.4 Immutability and Functional Data Structures
Immutability Principles
| Principle | Description | Benefit |
|---|---|---|
| No Mutation | Never modify existing data | Predictable state changes |
| Copy on Write | Create new copy when updating | Preserve history |
| Structural Sharing | Share unchanged parts | Memory efficiency |
| Persistent Data | All versions accessible | Time travel, undo/redo |
Immutable Operations
| Operation | Mutable (Avoid) | Immutable (Use) |
|---|---|---|
| Array Add | arr.push(item) | [...arr, item] or arr.concat(item) |
| Array Remove | arr.splice(idx, 1) | arr.filter((_, i) => i !== idx) |
| Array Update | arr[i] = value | arr.map((v, idx) => idx === i ? value : v) |
| Object Update | obj.prop = value | {...obj, prop: value} |
| Object Delete | delete obj.prop | const {prop, ...rest} = obj; return rest; |
| Object Merge | Object.assign(obj, updates) | {...obj, ...updates} |
Immutability Benefits
| Benefit | Description | Use Case |
|---|---|---|
| Predictability | Data doesn't change unexpectedly | Debugging, testing |
| Thread Safety | Safe concurrent access | Parallel processing |
| Change Detection | Reference equality checks | React shouldComponentUpdate |
| Time Travel | Keep all previous states | Undo/redo, debugging |
Example: Immutable operations
// MUTABLE (Avoid)
const arr = [1, 2, 3];
arr.push(4); // Modifies original
arr[0] = 10; // Modifies original
arr.sort(); // Modifies original
const obj = {name: 'John', age: 30};
obj.age = 31; // Modifies original
delete obj.name; // Modifies original
// IMMUTABLE (Prefer)
const arr2 = [1, 2, 3];
const arr3 = [...arr2, 4]; // New array
const arr4 = arr2.map((v, i) => i === 0 ? 10 : v); // New array
const arr5 = [...arr2].sort(); // New array
const obj2 = {name: 'John', age: 30};
const obj3 = {...obj2, age: 31}; // New object
const {name, ...obj4} = obj2; // New object without 'name'
// Array immutable operations
const numbers3 = [1, 2, 3, 4, 5];
// Add item
const withSix = [...numbers3, 6];
const withZero = [0, ...numbers3];
const withMiddle = [...numbers3.slice(0, 2), 99, ...numbers3.slice(2)];
// Remove item
const withoutThree = numbers3.filter(n => n !== 3);
const withoutIndex2 = numbers3.filter((_, i) => i !== 2);
// Update item
const doubled = numbers3.map(n => n * 2);
const updateIndex1 = numbers3.map((n, i) => i === 1 ? 99 : n);
// Object immutable operations
const user = {
name: 'John',
age: 30,
address: {
city: 'NYC',
country: 'USA'
}
};
// Update property
const olderUser = {...user, age: 31};
// Add property
const userWithEmail = {...user, email: 'john@example.com'};
// Remove property
const {age, ...userWithoutAge} = user;
// Deep update (nested)
const movedUser = {
...user,
address: {
...user.address,
city: 'LA'
}
};
// Nested updates helper
function updateNested(obj, path, value) {
const [first, ...rest] = path;
if (rest.length === 0) {
return {...obj, [first]: value};
}
return {
...obj,
[first]: updateNested(obj[first], rest, value)
};
}
const updated = updateNested(user, ['address', 'city'], 'LA');
console.log(updated);
// {name: 'John', age: 30, address: {city: 'LA', country: 'USA'}}
// Immutable array operations helper
const immutableArray = {
add: (arr, item) => [...arr, item],
addAt: (arr, index, item) => [
...arr.slice(0, index),
item,
...arr.slice(index)
],
remove: (arr, index) => [
...arr.slice(0, index),
...arr.slice(index + 1)
],
update: (arr, index, value) =>
arr.map((v, i) => i === index ? value : v),
updateWhere: (arr, predicate, updater) =>
arr.map(item => predicate(item) ? updater(item) : item)
};
const items = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
const newItems = immutableArray.updateWhere(
items,
item => item.id === 1,
item => ({...item, name: 'Updated A'})
);
console.log(newItems);
// Object.freeze for immutability
const frozen = Object.freeze({name: 'John', age: 30});
// frozen.age = 31; // Silent fail in non-strict, error in strict
// Deep freeze
function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null &&
(typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
!Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}
const deepFrozen = deepFreeze({
name: 'John',
address: {city: 'NYC'}
});
// deepFrozen.address.city = 'LA'; // Error
// Immutable state management
class ImmutableState {
constructor(initialState) {
this.state = initialState;
this.history = [initialState];
this.currentIndex = 0;
}
setState(updates) {
const newState = {...this.state, ...updates};
// Remove any "future" states if we went back
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(newState);
this.currentIndex++;
this.state = newState;
return newState;
}
getState() {
return this.state;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.state = this.history[this.currentIndex];
}
return this.state;
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
this.state = this.history[this.currentIndex];
}
return this.state;
}
canUndo() {
return this.currentIndex > 0;
}
canRedo() {
return this.currentIndex < this.history.length - 1;
}
}
// Usage
const state = new ImmutableState({count: 0, user: null});
state.setState({count: 1});
state.setState({count: 2});
state.setState({count: 3});
console.log(state.getState()); // {count: 3, user: null}
console.log(state.undo()); // {count: 2, user: null}
console.log(state.undo()); // {count: 1, user: null}
console.log(state.redo()); // {count: 2, user: null}
// Lens pattern for immutable updates
function lens(getter, setter) {
return {
get: getter,
set: setter,
over: (fn, obj) => setter(fn(getter(obj)), obj)
};
}
const nameLens = lens(
user => user.name,
(name, user) => ({...user, name})
);
const cityLens = lens(
user => user.address.city,
(city, user) => ({
...user,
address: {...user.address, city}
})
);
const user2 = {name: 'John', address: {city: 'NYC'}};
const user3 = nameLens.set('Jane', user2);
const user4 = cityLens.over(city => city.toUpperCase(), user2);
console.log(user3); // {name: 'Jane', address: {city: 'NYC'}}
console.log(user4); // {name: 'John', address: {city: 'NYC'}}
Example: Functional data structures
// Immutable List
class List {
constructor(head, tail = null) {
this.head = head;
this.tail = tail;
}
static empty() {
return null;
}
static of(...values) {
return values.reduceRight((tail, head) =>
new List(head, tail), null
);
}
prepend(value) {
return new List(value, this);
}
map(fn) {
if (this.tail === null) {
return new List(fn(this.head));
}
return new List(fn(this.head), this.tail.map(fn));
}
filter(predicate) {
if (!predicate(this.head)) {
return this.tail ? this.tail.filter(predicate) : null;
}
return new List(
this.head,
this.tail ? this.tail.filter(predicate) : null
);
}
toArray() {
const result = [];
let current = this;
while (current !== null) {
result.push(current.head);
current = current.tail;
}
return result;
}
}
// Usage
const list = List.of(1, 2, 3, 4, 5);
const doubled2 = list.map(x => x * 2);
const evens2 = list.filter(x => x % 2 === 0);
console.log(doubled2.toArray()); // [2, 4, 6, 8, 10]
console.log(evens2.toArray()); // [2, 4]
// Immutable Stack
class Stack {
constructor(items = []) {
this.items = Object.freeze([...items]);
}
push(item) {
return new Stack([...this.items, item]);
}
pop() {
if (this.items.length === 0) {
return [null, this];
}
return [
this.items[this.items.length - 1],
new Stack(this.items.slice(0, -1))
];
}
peek() {
return this.items[this.items.length - 1] || null;
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
}
// Usage
const stack1 = new Stack();
const stack2 = stack1.push(1);
const stack3 = stack2.push(2).push(3);
const [value, stack4] = stack3.pop();
console.log(value); // 3
console.log(stack4.peek()); // 2
console.log(stack3.peek()); // 3 (original unchanged)
// Immutable Queue
class Queue {
constructor(front = [], back = []) {
this.front = Object.freeze([...front]);
this.back = Object.freeze([...back]);
}
enqueue(item) {
return new Queue(this.front, [...this.back, item]);
}
dequeue() {
if (this.front.length === 0 && this.back.length === 0) {
return [null, this];
}
if (this.front.length === 0) {
const [first, ...rest] = this.back.reverse();
return [first, new Queue(rest, [])];
}
const [first, ...rest] = this.front;
return [first, new Queue(rest, this.back)];
}
peek() {
if (this.front.length > 0) {
return this.front[0];
}
return this.back[this.back.length - 1] || null;
}
isEmpty() {
return this.front.length === 0 && this.back.length === 0;
}
}
// Usage
const queue1 = new Queue();
const queue2 = queue1.enqueue(1).enqueue(2).enqueue(3);
const [val1, queue3] = queue2.dequeue();
const [val2, queue4] = queue3.dequeue();
console.log(val1); // 1
console.log(val2); // 2
// Immutable Tree
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
insert(value) {
if (value < this.value) {
return new TreeNode(
this.value,
this.left ? this.left.insert(value) : new TreeNode(value),
this.right
);
} else {
return new TreeNode(
this.value,
this.left,
this.right ? this.right.insert(value) : new TreeNode(value)
);
}
}
contains(value) {
if (value === this.value) return true;
if (value < this.value) return this.left ? this.left.contains(value) : false;
return this.right ? this.right.contains(value) : false;
}
map(fn) {
return new TreeNode(
fn(this.value),
this.left ? this.left.map(fn) : null,
this.right ? this.right.map(fn) : null
);
}
}
// Usage
const tree1 = new TreeNode(5);
const tree2 = tree1.insert(3).insert(7).insert(1).insert(9);
console.log(tree2.contains(7)); // true
console.log(tree2.contains(10)); // false
console.log(tree1.contains(7)); // false (original unchanged)
// Record type with immutable updates
class Record {
constructor(data) {
Object.entries(data).forEach(([key, value]) => {
Object.defineProperty(this, key, {
value,
writable: false,
enumerable: true
});
});
Object.freeze(this);
}
set(key, value) {
return new Record({...this, [key]: value});
}
merge(updates) {
return new Record({...this, ...updates});
}
}
// Usage
const person = new Record({name: 'John', age: 30});
const older = person.set('age', 31);
const updated2 = person.merge({age: 31, city: 'NYC'});
console.log(person); // Record {name: 'John', age: 30}
console.log(older); // Record {name: 'John', age: 31}
console.log(updated2); // Record {name: 'John', age: 31, city: 'NYC'}
Key Points: Immutability means never modifying existing data.
Use spread operator, array methods (map, filter, concat) for immutable updates.
Object.freeze()
prevents mutations. Deep freeze for nested objects. Immutable data enables time travel (undo/redo), change
detection, predictable state. Functional data structures (List, Stack, Queue, Tree) use structural sharing.
23.5 Monads and Functional Error Handling
Monad Characteristics
| Characteristic | Description | Law |
|---|---|---|
| Container | Wraps a value | Type constructor |
| of/return | Put value in container | Left identity |
| map/fmap | Transform value inside | Functor law |
| flatMap/bind | Chain operations | Right identity, associativity |
Common Monads
| Monad | Purpose | Use Case |
|---|---|---|
| Maybe/Option | Handle null/undefined | Nullable values |
| Either/Result | Handle success/failure | Error handling |
| Promise | Handle async operations | Async workflows |
| Array | Handle multiple values | Non-determinism |
| IO | Handle side effects | Lazy evaluation |
Functional Error Handling Patterns
| Pattern | Approach | Benefit |
|---|---|---|
| Maybe | null/undefined as type | Explicit optionality |
| Either | Left (error) or Right (success) | Error as value |
| Result | Ok or Err variant | Type-safe errors |
| Validation | Accumulate errors | Multiple validations |
Example: Maybe monad
// Maybe Monad - handles null/undefined
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
isNothing() {
return this.value === null || this.value === undefined;
}
map(fn) {
return this.isNothing() ? Maybe.nothing() : Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing() ? Maybe.nothing() : fn(this.value);
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
filter(predicate) {
return this.isNothing() || !predicate(this.value)
? Maybe.nothing()
: this;
}
}
// Usage
const safeDivide = (a, b) =>
b === 0 ? Maybe.nothing() : Maybe.of(a / b);
const result5 = safeDivide(10, 2)
.map(x => x * 2)
.map(x => x + 5)
.getOrElse(0);
console.log(result5); // 15
const result6 = safeDivide(10, 0)
.map(x => x * 2) // Not executed
.map(x => x + 5) // Not executed
.getOrElse(0);
console.log(result6); // 0
// Safe property access
const getNestedProp = (obj, path) => {
return path.reduce((maybe, prop) =>
maybe.flatMap(o => Maybe.of(o[prop])),
Maybe.of(obj)
);
};
const user5 = {
name: 'John',
address: {
city: 'NYC'
}
};
const city = getNestedProp(user5, ['address', 'city']).getOrElse('Unknown');
const zip = getNestedProp(user5, ['address', 'zip']).getOrElse('Unknown');
console.log(city); // NYC
console.log(zip); // Unknown
// Maybe with find
const findUser = (id) => {
const users3 = [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}];
const user = users3.find(u => u.id === id);
return user ? Maybe.of(user) : Maybe.nothing();
};
const userName = findUser(1)
.map(u => u.name)
.map(name => name.toUpperCase())
.getOrElse('Not Found');
console.log(userName); // JOHN
const missing = findUser(999)
.map(u => u.name)
.getOrElse('Not Found');
console.log(missing); // Not Found
Example: Either monad and Result pattern
// Either Monad - Left (error) or Right (success)
class Either {
constructor(value, isLeft = false) {
this.value = value;
this.isLeft = isLeft;
}
static left(value) {
return new Either(value, true);
}
static right(value) {
return new Either(value, false);
}
map(fn) {
return this.isLeft ? this : Either.right(fn(this.value));
}
flatMap(fn) {
return this.isLeft ? this : fn(this.value);
}
mapLeft(fn) {
return this.isLeft ? Either.left(fn(this.value)) : this;
}
fold(leftFn, rightFn) {
return this.isLeft ? leftFn(this.value) : rightFn(this.value);
}
getOrElse(defaultValue) {
return this.isLeft ? defaultValue : this.value;
}
}
// Usage with validation
const validateAge = (age) => {
if (typeof age !== 'number') {
return Either.left('Age must be a number');
}
if (age < 0) {
return Either.left('Age must be positive');
}
if (age < 18) {
return Either.left('Must be 18 or older');
}
return Either.right(age);
};
const result7 = validateAge(25)
.map(age => age * 2)
.fold(
error => `Error: ${error}`,
value => `Success: ${value}`
);
console.log(result7); // Success: 50
const result8 = validateAge(15)
.map(age => age * 2) // Not executed
.fold(
error => `Error: ${error}`,
value => `Success: ${value}`
);
console.log(result8); // Error: Must be 18 or older
// Result pattern (Rust-inspired)
class Result {
static ok(value) {
return {
isOk: true,
isErr: false,
value,
error: null
};
}
static err(error) {
return {
isOk: false,
isErr: true,
value: null,
error
};
}
}
// Usage
function parseJSON(str) {
try {
return Result.ok(JSON.parse(str));
} catch (error) {
return Result.err(error.message);
}
}
const validJSON = parseJSON('{"name": "John"}');
console.log(validJSON); // {isOk: true, value: {name: 'John'}, ...}
const invalidJSON = parseJSON('invalid json');
console.log(invalidJSON); // {isErr: true, error: '...', ...}
// Chain Result operations
function divideResult(a, b) {
return b === 0
? Result.err('Division by zero')
: Result.ok(a / b);
}
function sqrtResult(n) {
return n < 0
? Result.err('Cannot sqrt negative number')
: Result.ok(Math.sqrt(n));
}
function calculate(a, b) {
const divResult = divideResult(a, b);
if (divResult.isErr) {
return divResult;
}
return sqrtResult(divResult.value);
}
console.log(calculate(16, 4)); // {isOk: true, value: 2}
console.log(calculate(16, 0)); // {isErr: true, error: 'Division by zero'}
console.log(calculate(-16, 4)); // {isErr: true, error: 'Cannot sqrt negative...'}
// Try-catch wrapper
function tryCatch(fn) {
return (...args) => {
try {
return Either.right(fn(...args));
} catch (error) {
return Either.left(error);
}
};
}
const safeJSONParse = tryCatch(JSON.parse);
const result9 = safeJSONParse('{"valid": true}');
console.log(result9.fold(
err => `Parse error: ${err.message}`,
data => `Parsed: ${JSON.stringify(data)}`
));
// Validation with error accumulation
class Validation {
constructor(value, errors = []) {
this.value = value;
this.errors = errors;
}
static success(value) {
return new Validation(value, []);
}
static failure(error) {
return new Validation(null, [error]);
}
isValid() {
return this.errors.length === 0;
}
map(fn) {
return this.isValid()
? Validation.success(fn(this.value))
: this;
}
chain(fn) {
if (!this.isValid()) {
return this;
}
const result = fn(this.value);
return new Validation(result.value, [...this.errors, ...result.errors]);
}
}
// Validate user input
function validateUser(data) {
const errors = [];
if (!data.email || !data.email.includes('@')) {
errors.push('Invalid email');
}
if (!data.password || data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!data.age || data.age < 18) {
errors.push('Must be 18 or older');
}
return errors.length === 0
? Validation.success(data)
: new Validation(null, errors);
}
const validation1 = validateUser({
email: 'john@example.com',
password: 'secret123',
age: 25
});
console.log(validation1); // {value: {...}, errors: []}
const validation2 = validateUser({
email: 'invalid',
password: 'short',
age: 15
});
console.log(validation2);
// {value: null, errors: ['Invalid email', 'Password...', 'Must be 18...']}
Key Points: Monads are containers with map and flatMap methods.
Maybe monad handles null/undefined safely. Either
monad represents success (Right) or failure (Left). Result pattern from Rust (Ok/Err). Validation monad
accumulates errors. Functional error handling treats errors as values, not exceptions. Railway-oriented
programming with Either/Result enables composable error handling.
23.6 Function Pipelines and Data Transformation
Pipeline Patterns
| Pattern | Flow | Usage |
|---|---|---|
| Pipe | Left-to-right | data |> fn1 |> fn2 |> fn3 |
| Compose | Right-to-left | fn3(fn2(fn1(data))) |
| Method Chain | Sequential methods | obj.method1().method2() |
| Transducer | Composed transformations | Single pass over data |
Data Transformation Operations
| Operation | Purpose | Example |
|---|---|---|
| Map | Transform each element | arr.map(x => x * 2) |
| Filter | Select elements | arr.filter(x => x > 5) |
| Reduce | Aggregate to single value | arr.reduce((sum, x) => sum + x) |
| FlatMap | Map then flatten | arr.flatMap(x => [x, x*2]) |
| GroupBy | Group by property | Custom reduce |
| Partition | Split by predicate | [pass, fail] arrays |
Pipeline Benefits
| Benefit | Description | Impact |
|---|---|---|
| Readability | Sequential, declarative | Easier to understand |
| Composability | Build from small functions | Reusable components |
| Testability | Test each step independently | Better test coverage |
| Maintainability | Easy to add/remove steps | Flexible architecture |
Example: Pipeline operators and data transformation
// Pipe function (left-to-right)
const pipe2 = (...fns) => (value) =>
fns.reduce((acc, fn) => fn(acc), value);
// Example: Process user data
const users4 = [
{name: 'john doe', age: 17, active: true, score: 85},
{name: 'jane smith', age: 25, active: false, score: 92},
{name: 'bob jones', age: 30, active: true, score: 78},
{name: 'alice brown', age: 22, active: true, score: 95}
];
// Small, focused functions
const filterActive2 = users => users.filter(u => u.active);
const filterAdults2 = users => users.filter(u => u.age >= 18);
const sortByScore = users => [...users].sort((a, b) => b.score - a.score);
const takeFive = users => users.slice(0, 5);
const capitalizeNames2 = users => users.map(u => ({
...u,
name: u.name.split(' ').map(w =>
w.charAt(0).toUpperCase() + w.slice(1)
).join(' ')
}));
const extractNames2 = users => users.map(u => u.name);
// Build pipeline
const processUsers2 = pipe2(
filterActive2,
filterAdults2,
sortByScore,
takeFive,
capitalizeNames2,
extractNames2
);
console.log(processUsers2(users4));
// ['Alice Brown', 'Bob Jones']
// Alternative: Point-free style
const getTopActiveAdultNames = pipe2(
filterActive2,
filterAdults2,
sortByScore,
takeFive,
capitalizeNames2,
extractNames2
);
// Data transformation pipeline
const numbers4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const transform = pipe2(
arr => arr.filter(x => x % 2 === 0),
arr => arr.map(x => x * x),
arr => arr.reduce((sum, x) => sum + x, 0),
sum => Math.sqrt(sum)
);
console.log(transform(numbers4)); // Square root of sum of squares of evens
// Async pipeline
const asyncPipe2 = (...fns) => async (value) => {
let result = value;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
const fetchUserData = async (id) => {
// Simulated fetch
return {id, name: 'John', posts: []};
};
const enrichWithPosts = async (user) => {
// Simulated fetch
const posts = [{id: 1, title: 'Post 1'}];
return {...user, posts};
};
const formatUserData = (user) => ({
username: user.name.toUpperCase(),
postCount: user.posts.length
});
const getUserInfo = asyncPipe2(
fetchUserData,
enrichWithPosts,
formatUserData
);
// await getUserInfo(123);
// Tap function for debugging
const tap = (fn) => (value) => {
fn(value);
return value;
};
const debugPipeline = pipe2(
filterActive2,
tap(data => console.log('After filter:', data.length)),
filterAdults2,
tap(data => console.log('After adults filter:', data.length)),
sortByScore,
tap(data => console.log('After sort:', data[0]))
);
// GroupBy implementation
const groupBy = (key) => (array) => {
return array.reduce((groups, item) => {
const group = item[key];
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(item);
return groups;
}, {});
};
const byAge = groupBy('age');
const grouped = byAge(users4);
console.log(grouped);
// Partition function
const partition = (predicate) => (array) => {
return array.reduce(
([pass, fail], item) =>
predicate(item)
? [[...pass, item], fail]
: [pass, [...fail, item]],
[[], []]
);
};
const [adults, minors] = partition(u => u.age >= 18)(users4);
console.log('Adults:', adults.length);
console.log('Minors:', minors.length);
// Map with index
const mapWithIndex = (fn) => (array) =>
array.map((item, index) => fn(item, index));
const addIndex = mapWithIndex((item, index) => ({
...item,
index
}));
// Chunk array
const chunk = (size) => (array) => {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
};
const chunked = chunk(3)([1, 2, 3, 4, 5, 6, 7, 8]);
console.log(chunked); // [[1,2,3], [4,5,6], [7,8]]
// Flatten array
const flatten = (array) =>
array.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val),
[]);
const nested = [1, [2, [3, 4], 5], 6];
console.log(flatten(nested)); // [1, 2, 3, 4, 5, 6]
// Unique values
const unique = (array) => [...new Set(array)];
const duplicates = [1, 2, 2, 3, 3, 3, 4];
console.log(unique(duplicates)); // [1, 2, 3, 4]
// Pluck property
const pluck = (prop) => (array) =>
array.map(item => item[prop]);
const names = pluck('name')(users4);
console.log(names);
// Complex transformation pipeline
const processOrders = pipe2(
// Filter valid orders
orders => orders.filter(o => o.items.length > 0),
// Add total
orders => orders.map(o => ({
...o,
total: o.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
})),
// Filter high value
orders => orders.filter(o => o.total > 100),
// Sort by total
orders => [...orders].sort((a, b) => b.total - a.total),
// Group by customer
groupBy('customerId')
);
const orders = [
{customerId: 1, items: [{price: 10, quantity: 2}]},
{customerId: 1, items: [{price: 50, quantity: 3}]},
{customerId: 2, items: [{price: 20, quantity: 1}]},
{customerId: 2, items: [{price: 100, quantity: 2}]}
];
console.log(processOrders(orders));
// Memoized pipeline
const memoizePipeline = (pipeline) => {
const cache = new Map();
return (input) => {
const key = JSON.stringify(input);
if (cache.has(key)) {
return cache.get(key);
}
const result = pipeline(input);
cache.set(key, result);
return result;
};
};
const expensivePipeline = memoizePipeline(
pipe2(
arr => arr.map(x => x * x),
arr => arr.filter(x => x > 10),
arr => arr.reduce((sum, x) => sum + x, 0)
)
);
console.log(expensivePipeline([1, 2, 3, 4, 5])); // Computed
console.log(expensivePipeline([1, 2, 3, 4, 5])); // Cached
Key Points: Function pipelines compose small functions into
complex transformations. Pipe (left-to-right) vs compose (right-to-left). Common operations: map, filter,
reduce, flatMap, groupBy, partition. Benefits: readability, testability, maintainability. Use tap for
debugging. Async pipelines for async operations. Memoize pipelines for performance. Build complex data
transformations from simple, pure functions.
Section 23 Summary: Functional Programming Concepts
- Pure Functions: Deterministic, no side effects, same input → same output, easier to test and reason about
- Side Effects: Mutation, I/O, DOM, network, random - isolate at program boundaries, use pure core/impure shell
- Higher-Order Functions: Accept functions as arguments or return functions (map, filter, reduce, decorators)
- Function Composition: Combine functions with pipe (left-to-right) or compose (right-to-left)
- Currying: Transform f(a,b,c) to f(a)(b)(c), one argument at a time, enables partial application
- Partial Application: Fix some arguments, return function expecting remaining args, use bind or custom wrapper
- Immutability: Never modify data, use spread/map/filter for updates, Object.freeze, functional data structures
- Data Structures: Immutable List, Stack, Queue, Tree - structural sharing for efficiency
- Monads: Containers with map/flatMap - Maybe (null handling), Either (error handling), Result pattern
- Error Handling: Errors as values not exceptions, Either for success/failure, Validation for error accumulation
- Pipelines: Compose transformations (map, filter, reduce, groupBy, partition) for data processing
- Benefits: Functional programming enables predictability, testability, composability, maintainability, fewer bugs
24. Testing and Code Quality
24.1 Unit Testing Strategies and Test Structure
Unit Test Principles
| Principle | Description | Benefit |
|---|---|---|
| Isolation | Test one unit independently | Pinpoint failures |
| Fast | Run quickly (<100ms each) | Rapid feedback |
| Repeatable | Same result every time | Reliable CI/CD |
| Self-validating | Pass or fail, no manual check | Automation friendly |
| Timely | Written before/with code | Better design |
Test Structure (AAA Pattern)
| Phase | Purpose | Activities |
|---|---|---|
| Arrange | Setup test preconditions | Create objects, set state, prepare inputs |
| Act | Execute the code under test | Call method, trigger action |
| Assert | Verify expected outcome | Check results, validate state |
Test Naming Conventions
| Pattern | Format | Example |
|---|---|---|
| MethodName_StateUnderTest_ExpectedBehavior | method_condition_result | divide_byZero_throwsError |
| should_ExpectedBehavior_When_StateUnderTest | should_result_when_condition | should_throwError_when_dividingByZero |
| Given_When_Then | given_when_then | givenEmptyCart_whenAddingItem_thenCartHasOneItem |
| Descriptive sentence | Natural language | returns sum of two numbers |
Test Organization
| Level | Purpose | Syntax |
|---|---|---|
| describe | Group related tests | describe('Calculator', () => {}) |
| it/test | Individual test case | it('adds two numbers', () => {}) |
| beforeEach | Setup before each test | beforeEach(() => {}) |
| afterEach | Cleanup after each test | afterEach(() => {}) |
| beforeAll | Setup once before all tests | beforeAll(() => {}) |
| afterAll | Cleanup once after all tests | afterAll(() => {}) |
Example: Unit test structure
// Function to test
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Arguments must be numbers');
}
return a + b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// Unit tests using Jest/Vitest syntax
describe('Math operations', () => {
// Test suite for add function
describe('add', () => {
// AAA Pattern: Arrange, Act, Assert
it('should add two positive numbers', () => {
// Arrange
const a = 2;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(5);
});
it('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
it('should handle zero', () => {
expect(add(5, 0)).toBe(5);
expect(add(0, 5)).toBe(5);
});
it('should throw error for non-number inputs', () => {
expect(() => add('2', 3)).toThrow('Arguments must be numbers');
expect(() => add(2, null)).toThrow('Arguments must be numbers');
});
});
// Test suite for divide function
describe('divide', () => {
it('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should handle decimals', () => {
expect(divide(5, 2)).toBe(2.5);
});
it('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
// Testing a class
class Calculator {
constructor() {
this.result = 0;
}
add(n) {
this.result += n;
return this;
}
subtract(n) {
this.result -= n;
return this;
}
multiply(n) {
this.result *= n;
return this;
}
getResult() {
return this.result;
}
clear() {
this.result = 0;
return this;
}
}
describe('Calculator', () => {
let calculator;
// Setup before each test
beforeEach(() => {
calculator = new Calculator();
});
// Cleanup after each test (if needed)
afterEach(() => {
calculator = null;
});
describe('add', () => {
it('should add number to result', () => {
calculator.add(5);
expect(calculator.getResult()).toBe(5);
});
it('should support method chaining', () => {
calculator.add(5).add(3);
expect(calculator.getResult()).toBe(8);
});
});
describe('subtract', () => {
it('should subtract number from result', () => {
calculator.add(10).subtract(3);
expect(calculator.getResult()).toBe(7);
});
});
describe('multiply', () => {
it('should multiply result by number', () => {
calculator.add(5).multiply(3);
expect(calculator.getResult()).toBe(15);
});
});
describe('clear', () => {
it('should reset result to zero', () => {
calculator.add(10).clear();
expect(calculator.getResult()).toBe(0);
});
});
describe('complex operations', () => {
it('should handle chained operations', () => {
calculator
.add(10)
.subtract(3)
.multiply(2);
expect(calculator.getResult()).toBe(14);
});
});
});
// Testing async functions
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
describe('fetchUser', () => {
it('should fetch user data', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({id: 1, name: 'John'})
})
);
const user = await fetchUser(1);
expect(user).toEqual({id: 1, name: 'John'});
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
it('should throw error when user not found', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false
})
);
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
});
// Parameterized tests (test.each)
describe('add with multiple inputs', () => {
test.each([
[1, 1, 2],
[2, 3, 5],
[5, -5, 0],
[-10, -20, -30]
])('add(%i, %i) should return %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
// Testing edge cases
describe('Edge cases', () => {
it('should handle large numbers', () => {
expect(add(Number.MAX_SAFE_INTEGER, 1))
.toBe(Number.MAX_SAFE_INTEGER + 1);
});
it('should handle floating point precision', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
it('should handle negative zero', () => {
expect(add(-0, 0)).toBe(0);
});
});
// Snapshot testing (for objects/arrays)
describe('User object', () => {
it('should match snapshot', () => {
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
createdAt: new Date('2024-01-01')
};
expect(user).toMatchSnapshot();
});
});
// Testing with setup and teardown
describe('Database operations', () => {
let db;
beforeAll(async () => {
// Setup: connect to test database
db = await connectToTestDB();
});
afterAll(async () => {
// Teardown: disconnect from database
await db.disconnect();
});
beforeEach(async () => {
// Reset database state before each test
await db.clear();
});
it('should insert user', async () => {
const user = await db.users.insert({name: 'John'});
expect(user.id).toBeDefined();
expect(user.name).toBe('John');
});
it('should find user by id', async () => {
const inserted = await db.users.insert({name: 'Jane'});
const found = await db.users.findById(inserted.id);
expect(found).toEqual(inserted);
});
});
// Skip and only for focused testing
describe('Feature tests', () => {
it('should run this test', () => {
expect(true).toBe(true);
});
it.skip('should skip this test', () => {
// This test will be skipped
expect(false).toBe(true);
});
it.only('should only run this test', () => {
// Only this test will run (useful for debugging)
expect(true).toBe(true);
});
});
Key Points: Unit tests test individual units in isolation.
Follow AAA pattern: Arrange, Act, Assert. Use descriptive test names. Organize with describe/it blocks.
Setup/teardown with beforeEach/afterEach. Test edge cases, error conditions. Use test.each for parameterized
tests. Keep tests fast (<100ms), repeatable, independent. One assertion per test (generally).
24.2 Mocking and Stubbing Techniques
Test Doubles Types
| Type | Purpose | Usage |
|---|---|---|
| Mock | Verify interactions | Track calls, arguments, return values |
| Stub | Provide predefined responses | Return fixed values |
| Spy | Wrap real function | Track calls but execute real code |
| Fake | Working implementation | Simplified version (in-memory DB) |
| Dummy | Placeholder | Passed but never used |
Jest Mock Methods
| Method | Purpose | Example |
|---|---|---|
| jest.fn() | Create mock function | const mock = jest.fn() |
| jest.spyOn() | Spy on existing method | jest.spyOn(obj, 'method') |
| mockReturnValue() | Set return value | mock.mockReturnValue(42) |
| mockResolvedValue() | Resolve with value | mock.mockResolvedValue(data) |
| mockRejectedValue() | Reject with error | mock.mockRejectedValue(error) |
| mockImplementation() | Custom implementation | mock.mockImplementation(fn) |
Mock Assertions
| Assertion | Purpose | Usage |
|---|---|---|
| toHaveBeenCalled() | Verify called | expect(mock).toHaveBeenCalled() |
| toHaveBeenCalledTimes() | Verify call count | expect(mock).toHaveBeenCalledTimes(3) |
| toHaveBeenCalledWith() | Verify arguments | expect(mock).toHaveBeenCalledWith(arg1, arg2) |
| toHaveBeenLastCalledWith() | Verify last call arguments | expect(mock).toHaveBeenLastCalledWith(arg) |
| toHaveReturnedWith() | Verify return value | expect(mock).toHaveReturnedWith(42) |
Example: Mocking and stubbing
// Function using dependencies
class UserService {
constructor(api, logger) {
this.api = api;
this.logger = logger;
}
async getUser(id) {
this.logger.log(`Fetching user ${id}`);
try {
const user = await this.api.get(`/users/${id}`);
this.logger.log(`User ${id} fetched successfully`);
return user;
} catch (error) {
this.logger.error(`Failed to fetch user ${id}`, error);
throw error;
}
}
async createUser(data) {
this.logger.log('Creating user');
return this.api.post('/users', data);
}
}
// Testing with mocks
describe('UserService', () => {
let userService;
let mockApi;
let mockLogger;
beforeEach(() => {
// Create mock objects
mockApi = {
get: jest.fn(),
post: jest.fn()
};
mockLogger = {
log: jest.fn(),
error: jest.fn()
};
userService = new UserService(mockApi, mockLogger);
});
describe('getUser', () => {
it('should fetch user from API', async () => {
// Arrange
const mockUser = {id: 1, name: 'John'};
mockApi.get.mockResolvedValue(mockUser);
// Act
const result = await userService.getUser(1);
// Assert
expect(result).toEqual(mockUser);
expect(mockApi.get).toHaveBeenCalledWith('/users/1');
expect(mockLogger.log).toHaveBeenCalledWith('Fetching user 1');
expect(mockLogger.log).toHaveBeenCalledWith('User 1 fetched successfully');
});
it('should log error when fetch fails', async () => {
// Arrange
const error = new Error('Network error');
mockApi.get.mockRejectedValue(error);
// Act & Assert
await expect(userService.getUser(1)).rejects.toThrow('Network error');
expect(mockLogger.error).toHaveBeenCalledWith(
'Failed to fetch user 1',
error
);
});
});
describe('createUser', () => {
it('should create user via API', async () => {
const userData = {name: 'Jane', email: 'jane@example.com'};
const createdUser = {id: 2, ...userData};
mockApi.post.mockResolvedValue(createdUser);
const result = await userService.createUser(userData);
expect(result).toEqual(createdUser);
expect(mockApi.post).toHaveBeenCalledWith('/users', userData);
expect(mockLogger.log).toHaveBeenCalledWith('Creating user');
});
});
});
// Spying on existing methods
describe('Math operations with spy', () => {
it('should spy on Math.random', () => {
const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
const result = Math.random();
expect(result).toBe(0.5);
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // Restore original implementation
});
it('should spy on console.log', () => {
const spy = jest.spyOn(console, 'log').mockImplementation();
console.log('Hello');
console.log('World');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith('Hello');
expect(spy).toHaveBeenCalledWith('World');
spy.mockRestore();
});
});
// Mock implementations
describe('Mock implementations', () => {
it('should use custom implementation', () => {
const mockFn = jest.fn((a, b) => a + b);
expect(mockFn(2, 3)).toBe(5);
expect(mockFn(5, 7)).toBe(12);
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should return different values per call', () => {
const mockFn = jest.fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mockFn()).toBe('first');
expect(mockFn()).toBe('second');
expect(mockFn()).toBe('default');
expect(mockFn()).toBe('default');
});
it('should handle async operations', async () => {
const mockFn = jest.fn()
.mockResolvedValueOnce({id: 1})
.mockRejectedValueOnce(new Error('Failed'));
await expect(mockFn()).resolves.toEqual({id: 1});
await expect(mockFn()).rejects.toThrow('Failed');
});
});
// Mocking modules
// math.js
function add2(a, b) {
return a + b;
}
function multiply2(a, b) {
return a * b;
}
// calculator.js
function calculate2(operation, a, b) {
if (operation === 'add') {
return add2(a, b);
} else if (operation === 'multiply') {
return multiply2(a, b);
}
}
// calculator.test.js
jest.mock('./math', () => ({
add: jest.fn((a, b) => 100), // Always return 100
multiply: jest.fn((a, b) => 200)
}));
describe('calculator with mocked module', () => {
it('should use mocked add', () => {
const result = calculate2('add', 2, 3);
expect(result).toBe(100); // Uses mocked value
});
});
// Partial mocking
jest.mock('./math', () => {
const actual = jest.requireActual('./math');
return {
...actual,
add: jest.fn((a, b) => 100) // Only mock add, keep multiply real
};
});
// Timer mocks
describe('Timer mocks', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should execute after timeout', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
it('should execute interval', () => {
const callback = jest.fn();
setInterval(callback, 1000);
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
});
});
// Mocking fetch
describe('Fetch mocking', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it('should fetch user data', async () => {
const mockUser = {id: 1, name: 'John'};
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser
});
const response = await fetch('/api/users/1');
const data = await response.json();
expect(data).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
it('should handle fetch error', async () => {
global.fetch.mockResolvedValue({
ok: false,
status: 404
});
const response = await fetch('/api/users/999');
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
});
});
// Manual mocks
class MockLocalStorage {
constructor() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = value.toString();
}
removeItem(key) {
delete this.store[key];
}
clear() {
this.store = {};
}
}
describe('LocalStorage operations', () => {
let mockStorage;
beforeEach(() => {
mockStorage = new MockLocalStorage();
global.localStorage = mockStorage;
});
it('should store and retrieve item', () => {
localStorage.setItem('key', 'value');
expect(localStorage.getItem('key')).toBe('value');
});
it('should remove item', () => {
localStorage.setItem('key', 'value');
localStorage.removeItem('key');
expect(localStorage.getItem('key')).toBeNull();
});
});
Key Points: Mocks verify interactions (calls, arguments).
Stubs provide fixed responses. Spies track calls
while executing real code. Use jest.fn() for mocks, jest.spyOn() for spies. Mock return values with
mockReturnValue, mockResolvedValue. Assert with toHaveBeenCalled, toHaveBeenCalledWith. Mock modules with
jest.mock(). Use fake timers for testing setTimeout/setInterval.
24.3 Test-Driven Development Patterns
TDD Cycle (Red-Green-Refactor)
| Phase | Action | Goal |
|---|---|---|
| Red | Write failing test | Define expected behavior |
| Green | Write minimal code to pass | Make test pass quickly |
| Refactor | Improve code quality | Clean up while maintaining tests |
TDD Benefits
| Benefit | Description | Impact |
|---|---|---|
| Better Design | Forces thinking about API first | More modular, testable code |
| Documentation | Tests document expected behavior | Living specifications |
| Confidence | Safety net for changes | Fearless refactoring |
| Regression Prevention | Catch bugs early | Fewer production issues |
| Focus | One requirement at a time | Incremental progress |
TDD Best Practices
| Practice | Description | Example |
|---|---|---|
| Small Steps | Test one thing at a time | One assertion per test |
| Test First | Always write test before code | Define API through tests |
| Simplest Solution | Write minimal code to pass | Avoid over-engineering |
| Refactor Often | Clean up after green | Remove duplication, improve names |
| Fast Feedback | Run tests frequently | Catch issues immediately |
Example: TDD workflow
// Example: Building a shopping cart with TDD
// STEP 1: RED - Write failing test
describe('ShoppingCart', () => {
it('should start empty', () => {
const cart = new ShoppingCart();
expect(cart.getItemCount()).toBe(0);
});
});
// Run test → FAIL (ShoppingCart doesn't exist)
// STEP 2: GREEN - Write minimal code to pass
class ShoppingCart {
constructor() {
this.items = [];
}
getItemCount() {
return 0; // Hardcoded to pass test
}
}
// Run test → PASS
// STEP 3: REFACTOR - (none needed yet)
// STEP 4: RED - Next test
describe('ShoppingCart', () => {
it('should start empty', () => {
const cart = new ShoppingCart();
expect(cart.getItemCount()).toBe(0);
});
it('should add item to cart', () => {
const cart = new ShoppingCart();
cart.addItem({name: 'Apple', price: 1.5});
expect(cart.getItemCount()).toBe(1);
});
});
// Run test → FAIL (addItem doesn't exist)
// STEP 5: GREEN - Implement addItem
class ShoppingCart2 {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
getItemCount() {
return this.items.length; // Now dynamic
}
}
// Run test → PASS
// STEP 6: RED - Test multiple items
it('should add multiple items', () => {
const cart = new ShoppingCart2();
cart.addItem({name: 'Apple', price: 1.5});
cart.addItem({name: 'Banana', price: 2.0});
expect(cart.getItemCount()).toBe(2);
});
// Run test → PASS (already works!)
// STEP 7: RED - Test total calculation
it('should calculate total', () => {
const cart = new ShoppingCart2();
cart.addItem({name: 'Apple', price: 1.5});
cart.addItem({name: 'Banana', price: 2.0});
expect(cart.getTotal()).toBe(3.5);
});
// Run test → FAIL (getTotal doesn't exist)
// STEP 8: GREEN - Implement getTotal
class ShoppingCart3 {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
getItemCount() {
return this.items.length;
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// Run test → PASS
// STEP 9: RED - Test remove item
it('should remove item from cart', () => {
const cart = new ShoppingCart3();
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.addItem({id: 2, name: 'Banana', price: 2.0});
cart.removeItem(1);
expect(cart.getItemCount()).toBe(1);
expect(cart.getTotal()).toBe(2.0);
});
// Run test → FAIL (removeItem doesn't exist)
// STEP 10: GREEN - Implement removeItem
class ShoppingCart4 {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
removeItem(id) {
this.items = this.items.filter(item => item.id !== id);
}
getItemCount() {
return this.items.length;
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// Run test → PASS
// STEP 11: REFACTOR - Extract duplicated test setup
describe('ShoppingCart (refactored)', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart4();
});
it('should start empty', () => {
expect(cart.getItemCount()).toBe(0);
});
describe('addItem', () => {
it('should add single item', () => {
cart.addItem({id: 1, name: 'Apple', price: 1.5});
expect(cart.getItemCount()).toBe(1);
});
it('should add multiple items', () => {
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.addItem({id: 2, name: 'Banana', price: 2.0});
expect(cart.getItemCount()).toBe(2);
});
});
describe('getTotal', () => {
it('should calculate total for multiple items', () => {
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.addItem({id: 2, name: 'Banana', price: 2.0});
expect(cart.getTotal()).toBe(3.5);
});
it('should return 0 for empty cart', () => {
expect(cart.getTotal()).toBe(0);
});
});
describe('removeItem', () => {
beforeEach(() => {
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.addItem({id: 2, name: 'Banana', price: 2.0});
});
it('should remove item', () => {
cart.removeItem(1);
expect(cart.getItemCount()).toBe(1);
expect(cart.getTotal()).toBe(2.0);
});
it('should handle removing non-existent item', () => {
cart.removeItem(999);
expect(cart.getItemCount()).toBe(2);
});
});
});
// TDD with edge cases
describe('ShoppingCart edge cases', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart4();
});
it('should handle item with quantity', () => {
cart.addItem({id: 1, name: 'Apple', price: 1.5, quantity: 3});
// This test will fail, driving us to add quantity support
expect(cart.getTotal()).toBe(4.5); // 1.5 * 3
});
it('should apply discount', () => {
cart.addItem({id: 1, name: 'Apple', price: 10});
cart.applyDiscount(0.1); // 10% discount
expect(cart.getTotal()).toBe(9);
});
it('should clear cart', () => {
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.clear();
expect(cart.getItemCount()).toBe(0);
expect(cart.getTotal()).toBe(0);
});
});
// Test-first for bug fixes
describe('Bug fix: duplicate items', () => {
it('should prevent adding duplicate items', () => {
const cart = new ShoppingCart4();
cart.addItem({id: 1, name: 'Apple', price: 1.5});
cart.addItem({id: 1, name: 'Apple', price: 1.5}); // Duplicate
expect(cart.getItemCount()).toBe(1); // Should only have 1 item
});
// This test fails, so we fix the implementation
});
// Implementation with duplicate prevention
class ShoppingCart5 {
constructor() {
this.items = [];
}
addItem(item) {
const exists = this.items.find(i => i.id === item.id);
if (!exists) {
this.items.push(item);
}
}
// ... rest of methods
}
Key Points: TDD cycle: Red (write failing test), Green (make
it pass), Refactor (improve code). Write test first to define API. Write minimal code to pass test. Refactor
with safety of tests. Benefits: better design, documentation, confidence. Test one thing at a time. Run tests
frequently for fast feedback. Use TDD for bug fixes: write test that reproduces bug, fix code, test passes.
24.4 Code Coverage and Quality Metrics
Coverage Types
| Type | Measures | Goal |
|---|---|---|
| Line Coverage | % of lines executed | 80-90% typically |
| Branch Coverage | % of if/else branches tested | More thorough than line |
| Function Coverage | % of functions called | All functions tested |
| Statement Coverage | % of statements executed | Similar to line coverage |
Quality Metrics
| Metric | Description | Tool |
|---|---|---|
| Cyclomatic Complexity | Number of execution paths | ESLint, SonarQube |
| Maintainability Index | Ease of maintenance score | Code Climate |
| Code Duplication | % of duplicated code | SonarQube, jscpd |
| Technical Debt | Time to fix issues | SonarQube |
| Test Quality | Mutation score, assertion density | Stryker |
Coverage Tools
| Tool | Purpose | Usage |
|---|---|---|
| Istanbul/nyc | Code coverage reporting | jest --coverage |
| Codecov | Coverage tracking service | CI/CD integration |
| Coveralls | Coverage history tracking | GitHub integration |
| SonarQube | Quality metrics platform | Enterprise quality gate |
Example: Code coverage
// Function with branches
function getDiscount(customer) {
if (customer.isPremium) {
if (customer.purchaseAmount > 100) {
return 0.2; // 20% discount
} else {
return 0.1; // 10% discount
}
} else {
if (customer.purchaseAmount > 50) {
return 0.05; // 5% discount
} else {
return 0; // No discount
}
}
}
// Tests for 100% branch coverage
describe('getDiscount', () => {
it('should give 20% discount for premium customer with high purchase', () => {
const customer = {isPremium: true, purchaseAmount: 150};
expect(getDiscount(customer)).toBe(0.2);
});
it('should give 10% discount for premium customer with low purchase', () => {
const customer = {isPremium: true, purchaseAmount: 50};
expect(getDiscount(customer)).toBe(0.1);
});
it('should give 5% discount for non-premium with medium purchase', () => {
const customer = {isPremium: false, purchaseAmount: 75};
expect(getDiscount(customer)).toBe(0.05);
});
it('should give no discount for non-premium with low purchase', () => {
const customer = {isPremium: false, purchaseAmount: 25};
expect(getDiscount(customer)).toBe(0);
});
});
// All 4 branches are now covered!
// Running coverage with Jest
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"test:coverage:watch": "jest --coverage --watch"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.test.js",
"!src/index.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
// Coverage report shows:
// File | % Stmts | % Branch | % Funcs | % Lines |
// ----------|---------|----------|---------|---------|
// discount.js | 100 | 100 | 100 | 100 |
// Uncovered code example
function processPayment(amount, method) {
if (method === 'credit') {
return chargeCreditCard(amount);
} else if (method === 'debit') {
return chargeDebitCard(amount);
} else if (method === 'paypal') {
return chargePayPal(amount);
}
// No else branch - what if method is invalid?
// Coverage report will show this as uncovered
}
// Better version with default case
function processPayment2(amount, method) {
if (method === 'credit') {
return chargeCreditCard(amount);
} else if (method === 'debit') {
return chargeDebitCard(amount);
} else if (method === 'paypal') {
return chargePayPal(amount);
} else {
throw new Error(`Unknown payment method: ${method}`);
}
}
// Test for error case
it('should throw error for invalid payment method', () => {
expect(() => processPayment2(100, 'bitcoin'))
.toThrow('Unknown payment method: bitcoin');
});
// Cyclomatic complexity example
// Bad: High complexity (hard to test, maintain)
function calculateShipping(weight, distance, isPremium, isInternational, isFragile) {
let cost = 0;
if (weight > 10) {
if (distance > 100) {
if (isPremium) {
if (isInternational) {
if (isFragile) {
cost = 100;
} else {
cost = 80;
}
} else {
cost = 50;
}
} else {
cost = 30;
}
} else {
cost = 20;
}
} else {
cost = 10;
}
return cost;
}
// Cyclomatic complexity: 7 (too high!)
// Good: Lower complexity (easier to test, maintain)
function calculateShipping2(options) {
const {weight, distance, isPremium, isInternational, isFragile} = options;
let cost = getBaseCost(weight, distance);
if (isPremium) {
cost = applyPremiumRate(cost);
}
if (isInternational) {
cost = applyInternationalRate(cost);
}
if (isFragile) {
cost = applyFragileHandling(cost);
}
return cost;
}
function getBaseCost(weight, distance) {
if (weight > 10 && distance > 100) return 30;
if (weight > 10) return 20;
return 10;
}
function applyPremiumRate(cost) {
return cost * 1.5;
}
function applyInternationalRate(cost) {
return cost * 2;
}
function applyFragileHandling(cost) {
return cost + 20;
}
// Each function has lower complexity, easier to test individually
// Mutation testing concept
function isPositive(n) {
return n > 0; // Mutant: change > to >=
}
// This test would NOT catch the mutation
it('should return true for positive number', () => {
expect(isPositive(5)).toBe(true);
});
// Better test suite that catches mutations
describe('isPositive', () => {
it('should return true for positive number', () => {
expect(isPositive(5)).toBe(true);
});
it('should return false for zero', () => {
expect(isPositive(0)).toBe(false); // Catches >= mutation
});
it('should return false for negative number', () => {
expect(isPositive(-5)).toBe(false);
});
});
// Coverage doesn't guarantee quality
function divide2(a, b) {
return a / b;
}
// 100% coverage, but missing important test
it('divides two numbers', () => {
expect(divide2(10, 2)).toBe(5);
});
// No test for division by zero!
// Better test suite
describe('divide', () => {
it('should divide two numbers', () => {
expect(divide2(10, 2)).toBe(5);
});
it('should handle division by zero', () => {
expect(divide2(10, 0)).toBe(Infinity);
});
it('should handle negative numbers', () => {
expect(divide2(-10, 2)).toBe(-5);
});
it('should handle decimals', () => {
expect(divide2(5, 2)).toBe(2.5);
});
});
// Code quality checks with ESLint
// .eslintrc.json
{
"extends": "eslint:recommended",
"rules": {
"complexity": ["error", 5], // Max cyclomatic complexity
"max-depth": ["error", 3], // Max nesting depth
"max-lines-per-function": ["warn", 50],
"max-params": ["error", 4], // Max function parameters
"no-duplicate-code": "error"
}
}
// Measuring test quality
describe('Test metrics', () => {
// Number of assertions per test
it('should have focused assertions', () => {
const user = createUser();
expect(user.name).toBeDefined(); // 1
expect(user.email).toBeDefined(); // 2
expect(user.id).toBeDefined(); // 3
// 3 assertions per test
});
// Test execution time
it('should run fast', () => {
const start = performance.now();
const result = someOperation();
const duration = performance.now() - start;
expect(duration).toBeLessThan(100); // Should be fast
});
});
// Coverage configuration for different file types
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.test.{js,jsx,ts,tsx}',
'!src/**/__tests__/**',
'!src/index.js',
'!src/setupTests.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/utils/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
coverageReporters: ['text', 'lcov', 'html']
};
Key Points: Code coverage measures % of code executed by tests.
Types: line, branch, function, statement coverage. Aim for 80-90% coverage. Branch
coverage more thorough than line coverage. Coverage doesn't guarantee quality - need good assertions.
Cyclomatic complexity measures code complexity. Mutation testing validates test quality. Use ESLint for
complexity limits. Configure coverage thresholds in jest.config.
24.5 Assertion Libraries and Testing Utilities
Common Assertion Libraries
| Library | Style | Usage |
|---|---|---|
| Jest | expect() | Built-in, batteries included |
| Chai | expect/should/assert | Flexible, plugin ecosystem |
| Vitest | expect() (Jest-compatible) | Vite-native, fast |
| Node assert | assert.equal() | Built-in Node.js |
Jest Matchers
| Matcher | Purpose | Example |
|---|---|---|
| toBe | Strict equality (===) | expect(value).toBe(5) |
| toEqual | Deep equality | expect(obj).toEqual({a: 1}) |
| toMatch | Regex match | expect(str).toMatch(/hello/) |
| toContain | Array/string contains | expect(arr).toContain(item) |
| toThrow | Function throws error | expect(fn).toThrow() |
| toBeTruthy/toBeFalsy | Boolean coercion | expect(value).toBeTruthy() |
| toBeNull/toBeUndefined | Null/undefined check | expect(val).toBeNull() |
| toBeGreaterThan | Numeric comparison | expect(5).toBeGreaterThan(3) |
| toHaveLength | Array/string length | expect(arr).toHaveLength(3) |
| toHaveProperty | Object property check | expect(obj).toHaveProperty('key') |
Testing Utilities
| Utility | Purpose | Library |
|---|---|---|
| Faker | Generate fake data | @faker-js/faker |
| Testing Library | DOM testing utilities | @testing-library/react |
| MSW | Mock service worker (API mocking) | msw |
| Supertest | HTTP assertion | supertest |
| Nock | HTTP mocking | nock |
Example: Assertions and matchers
// Basic matchers
describe('Basic matchers', () => {
it('toBe for primitives (===)', () => {
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');
expect(true).toBe(true);
});
it('toEqual for objects/arrays (deep equality)', () => {
const obj1 = {name: 'John', age: 30};
const obj2 = {name: 'John', age: 30};
expect(obj1).toEqual(obj2); // Pass
// expect(obj1).toBe(obj2); // Fail (different references)
expect([1, 2, 3]).toEqual([1, 2, 3]);
});
it('toStrictEqual (stricter than toEqual)', () => {
const obj = {a: 1, b: undefined};
expect(obj).toEqual({a: 1}); // Pass (ignores undefined)
expect(obj).toStrictEqual({a: 1}); // Fail (checks undefined)
});
});
// Number matchers
describe('Number matchers', () => {
it('comparison matchers', () => {
expect(5).toBeGreaterThan(3);
expect(3).toBeLessThan(5);
expect(5).toBeGreaterThanOrEqual(5);
expect(3).toBeLessThanOrEqual(3);
});
it('toBeCloseTo for floating point', () => {
expect(0.1 + 0.2).toBeCloseTo(0.3); // Handles floating point precision
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 decimal places
});
});
// String matchers
describe('String matchers', () => {
it('toMatch with regex', () => {
expect('hello world').toMatch(/world/);
expect('hello world').toMatch(/^hello/);
expect('test@example.com').toMatch(/^[^@]+@[^@]+\.[^@]+$/);
});
it('string contains', () => {
expect('hello world').toContain('world');
});
});
// Array matchers
describe('Array matchers', () => {
it('toContain for arrays', () => {
const arr = [1, 2, 3, 4, 5];
expect(arr).toContain(3);
expect(arr).toContain(5);
});
it('toHaveLength', () => {
expect([1, 2, 3]).toHaveLength(3);
expect('hello').toHaveLength(5);
});
it('toContainEqual for object arrays', () => {
const users = [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}];
expect(users).toContainEqual({id: 1, name: 'John'});
});
it('arrayContaining for partial match', () => {
expect([1, 2, 3, 4]).toEqual(expect.arrayContaining([2, 3]));
});
});
// Object matchers
describe('Object matchers', () => {
it('toHaveProperty', () => {
const user = {name: 'John', age: 30};
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('name', 'John');
expect(user).toHaveProperty('age', 30);
});
it('objectContaining for partial match', () => {
const user = {id: 1, name: 'John', email: 'john@example.com'};
expect(user).toEqual(expect.objectContaining({
name: 'John',
email: 'john@example.com'
}));
});
it('toMatchObject for subset', () => {
const user = {id: 1, name: 'John', age: 30};
expect(user).toMatchObject({name: 'John'});
expect(user).toMatchObject({name: 'John', age: 30});
});
});
// Error matchers
describe('Error matchers', () => {
it('toThrow', () => {
function throwError() {
throw new Error('Something went wrong');
}
expect(throwError).toThrow();
expect(throwError).toThrow(Error);
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(/went wrong/);
});
it('not.toThrow', () => {
function noError() {
return 'ok';
}
expect(noError).not.toThrow();
});
});
// Truthiness matchers
describe('Truthiness matchers', () => {
it('toBeTruthy/toBeFalsy', () => {
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect('hello').toBeTruthy();
expect([]).toBeTruthy();
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
});
it('toBeDefined/toBeUndefined', () => {
const obj = {name: 'John'};
expect(obj.name).toBeDefined();
expect(obj.age).toBeUndefined();
});
it('toBeNull', () => {
expect(null).toBeNull();
expect(undefined).not.toBeNull();
});
});
// Async matchers
describe('Async matchers', () => {
it('resolves', async () => {
const promise = Promise.resolve('success');
await expect(promise).resolves.toBe('success');
});
it('rejects', async () => {
const promise = Promise.reject(new Error('failed'));
await expect(promise).rejects.toThrow('failed');
});
});
// Custom matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false
};
}
},
toBeValidEmail(received) {
const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
return {
message: () => pass
? `expected ${received} not to be a valid email`
: `expected ${received} to be a valid email`,
pass
};
}
});
describe('Custom matchers', () => {
it('toBeWithinRange', () => {
expect(5).toBeWithinRange(1, 10);
expect(15).not.toBeWithinRange(1, 10);
});
it('toBeValidEmail', () => {
expect('user@example.com').toBeValidEmail();
expect('invalid-email').not.toBeValidEmail();
});
});
// Test data generation with Faker
// import { faker } from '@faker-js/faker';
function generateUser() {
return {
id: faker.datatype.uuid(),
name: faker.name.fullName(),
email: faker.internet.email(),
age: faker.datatype.number({min: 18, max: 80}),
address: {
street: faker.address.streetAddress(),
city: faker.address.city(),
country: faker.address.country()
}
};
}
describe('Faker data generation', () => {
it('generates realistic test data', () => {
const user = generateUser();
expect(user.id).toBeDefined();
expect(user.name).toMatch(/\w+ \w+/);
expect(user.email).toBeValidEmail();
expect(user.age).toBeGreaterThanOrEqual(18);
expect(user.age).toBeLessThanOrEqual(80);
});
it('generates multiple users', () => {
const users = Array.from({length: 5}, generateUser);
expect(users).toHaveLength(5);
expect(users[0].id).not.toBe(users[1].id); // Unique IDs
});
});
// Snapshot testing
describe('Snapshot testing', () => {
it('matches inline snapshot', () => {
const user = {id: 1, name: 'John', email: 'john@example.com'};
expect(user).toMatchInlineSnapshot(`
{
"email": "john@example.com",
"id": 1,
"name": "John",
}
`);
});
it('matches snapshot file', () => {
const component = renderComponent();
expect(component).toMatchSnapshot();
});
});
// Asymmetric matchers
describe('Asymmetric matchers', () => {
it('expect.anything()', () => {
expect({id: 1, name: 'John'}).toEqual({
id: expect.anything(),
name: 'John'
});
});
it('expect.any(constructor)', () => {
expect({id: 1, createdAt: new Date()}).toEqual({
id: 1,
createdAt: expect.any(Date)
});
});
it('expect.stringContaining()', () => {
expect('hello world').toEqual(expect.stringContaining('world'));
});
it('expect.stringMatching()', () => {
expect('test@example.com').toEqual(
expect.stringMatching(/^[^@]+@[^@]+\.[^@]+$/)
);
});
});
Key Points: Jest matchers: toBe (===), toEqual (deep), toMatch
(regex), toContain (array/string), toThrow (errors). Number matchers: toBeGreaterThan, toBeCloseTo (floats).
Object matchers: toHaveProperty, objectContaining, toMatchObject. Async: resolves, rejects. Create custom
matchers with expect.extend. Use Faker for test data generation. Snapshot testing for complex objects.
Asymmetric
matchers: expect.anything(), expect.any(Type).
24.6 Integration and End-to-End Testing
Testing Pyramid
| Level | Speed | Cost | Coverage | Quantity |
|---|---|---|---|---|
| Unit | Fast | Low | Small | Many (70%) |
| Integration | Medium | Medium | Medium | Some (20%) |
| E2E | Slow | High | Large | Few (10%) |
Integration Test Types
| Type | Tests | Example |
|---|---|---|
| Component Integration | Multiple units together | Service + Repository |
| API Integration | HTTP endpoints | REST API routes |
| Database Integration | DB operations | CRUD operations |
| External Service | Third-party APIs | Payment gateway |
E2E Testing Tools
| Tool | Purpose | Features |
|---|---|---|
| Cypress | Modern E2E testing | Real browser, time travel debugging |
| Playwright | Cross-browser E2E | Multi-browser, parallel, API testing |
| Puppeteer | Chrome automation | Headless Chrome, screenshots |
| WebdriverIO | WebDriver protocol | Mobile, desktop, cloud services |
Example: Integration testing
// Integration test: Service + Repository
class UserRepository {
constructor(db) {
this.db = db;
}
async create(userData) {
return this.db.users.insert(userData);
}
async findById(id) {
return this.db.users.findOne({id});
}
async update(id, updates) {
return this.db.users.update({id}, updates);
}
}
class UserService2 {
constructor(repository, emailService) {
this.repository = repository;
this.emailService = emailService;
}
async createUser(userData) {
const user = await this.repository.create(userData);
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
async updateUser(id, updates) {
const user = await this.repository.findById(id);
if (!user) {
throw new Error('User not found');
}
return this.repository.update(id, updates);
}
}
// Integration test
describe('UserService Integration', () => {
let db;
let repository;
let emailService;
let userService;
beforeAll(async () => {
// Setup test database
db = await setupTestDatabase();
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
// Clear database before each test
await db.users.clear();
// Create real repository
repository = new UserRepository(db);
// Mock email service
emailService = {
sendWelcomeEmail: jest.fn().mockResolvedValue(true)
};
userService = new UserService2(repository, emailService);
});
it('should create user and send welcome email', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const user = await userService.createUser(userData);
// Verify user created in database
expect(user.id).toBeDefined();
expect(user.name).toBe('John Doe');
// Verify can retrieve from database
const retrieved = await repository.findById(user.id);
expect(retrieved).toEqual(user);
// Verify email sent
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('john@example.com');
});
it('should update existing user', async () => {
// Create user first
const user = await repository.create({
name: 'John',
email: 'john@example.com'
});
// Update user
const updated = await userService.updateUser(user.id, {
name: 'John Updated'
});
expect(updated.name).toBe('John Updated');
// Verify in database
const retrieved = await repository.findById(user.id);
expect(retrieved.name).toBe('John Updated');
});
it('should throw error when updating non-existent user', async () => {
await expect(
userService.updateUser(999, {name: 'Test'})
).rejects.toThrow('User not found');
});
});
// API Integration testing with Supertest
// import request from 'supertest';
// import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/users', async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user);
});
app.get('/api/users/:id', async (req, res) => {
const user = await findUser(req.params.id);
if (!user) {
return res.status(404).json({error: 'User not found'});
}
res.json(user);
});
describe('User API', () => {
beforeAll(async () => {
await setupTestDatabase();
});
afterEach(async () => {
await clearDatabase();
});
describe('POST /api/users', () => {
it('should create user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John Doe',
email: 'john@example.com'
})
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toMatchObject({
name: 'John Doe',
email: 'john@example.com'
});
expect(response.body.id).toBeDefined();
});
it('should return 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({name: 'John'}) // Missing email
.expect(400);
});
});
describe('GET /api/users/:id', () => {
it('should get user by id', async () => {
// Create user first
const created = await createUser({
name: 'Jane',
email: 'jane@example.com'
});
const response = await request(app)
.get(`/api/users/${created.id}`)
.expect(200);
expect(response.body).toEqual(created);
});
it('should return 404 for non-existent user', async () => {
await request(app)
.get('/api/users/999')
.expect(404);
});
});
});
Example: E2E testing with Playwright
// Playwright E2E test
// import { test, expect } from '@playwright/test';
test.describe('Shopping Cart E2E', () => {
test.beforeEach(async ({ page }) => {
// Navigate to app
await page.goto('http://localhost:3000');
});
test('should add item to cart and checkout', async ({ page }) => {
// Navigate to products
await page.click('text=Products');
// Wait for products to load
await page.waitForSelector('.product-card');
// Add first product to cart
await page.click('.product-card:first-child button:has-text("Add to Cart")');
// Verify cart count updated
const cartCount = await page.textContent('.cart-count');
expect(cartCount).toBe('1');
// Go to cart
await page.click('text=Cart');
// Verify item in cart
await expect(page.locator('.cart-item')).toHaveCount(1);
// Verify total
const total = await page.textContent('.cart-total');
expect(total).toContain("$");
// Proceed to checkout
await page.click('button:has-text("Checkout")');
// Fill checkout form
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('input[name="address"]', '123 Main St');
await page.fill('input[name="cardNumber"]', '4111111111111111');
// Submit order
await page.click('button:has-text("Place Order")');
// Wait for confirmation
await page.waitForSelector('.order-confirmation');
// Verify success message
const message = await page.textContent('.order-confirmation h2');
expect(message).toContain('Order Confirmed');
// Verify order number
await expect(page.locator('.order-number')).toBeVisible();
});
test('should search for products', async ({ page }) => {
// Enter search term
await page.fill('input[placeholder="Search products"]', 'laptop');
await page.press('input[placeholder="Search products"]', 'Enter');
// Wait for results
await page.waitForSelector('.product-card');
// Verify all results contain search term
const products = await page.$('.product-card');
expect(products.length).toBeGreaterThan(0);
for (const product of products) {
const text = await product.textContent();
expect(text.toLowerCase()).toContain('laptop');
}
});
test('should handle empty cart checkout', async ({ page }) => {
// Try to checkout with empty cart
await page.click('text=Cart');
// Checkout button should be disabled
const checkoutBtn = page.locator('button:has-text("Checkout")');
await expect(checkoutBtn).toBeDisabled();
// Verify empty cart message
await expect(page.locator('text=Your cart is empty')).toBeVisible();
});
});
// Cypress E2E test
// describe('Login Flow', () => {
// beforeEach(() => {
// cy.visit('http://localhost:3000/login');
// });
//
// it('should login successfully', () => {
// // Fill form
// cy.get('input[name="email"]').type('user@example.com');
// cy.get('input[name="password"]').type('password123');
//
// // Submit
// cy.get('button[type="submit"]').click();
//
// // Verify redirect
// cy.url().should('include', '/dashboard');
//
// // Verify user info displayed
// cy.contains('Welcome, user@example.com');
// });
//
// it('should show error for invalid credentials', () => {
// cy.get('input[name="email"]').type('wrong@example.com');
// cy.get('input[name="password"]').type('wrongpass');
// cy.get('button[type="submit"]').click();
//
// // Verify error message
// cy.contains('Invalid credentials');
//
// // Should stay on login page
// cy.url().should('include', '/login');
// });
//
// it('should validate form fields', () => {
// // Try to submit empty form
// cy.get('button[type="submit"]').click();
//
// // Verify validation errors
// cy.contains('Email is required');
// cy.contains('Password is required');
// });
// });
// Visual regression testing
test('should match screenshot', async ({ page }) => {
await page.goto('http://localhost:3000');
// Take full page screenshot
await expect(page).toHaveScreenshot('homepage.png');
// Take element screenshot
const header = page.locator('header');
await expect(header).toHaveScreenshot('header.png');
});
// Mobile testing
test('should work on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('http://localhost:3000');
// Verify mobile menu
await page.click('.mobile-menu-icon');
await expect(page.locator('.mobile-menu')).toBeVisible();
});
// Performance testing
test('should load within 3 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000);
});
Key Points: Integration tests test multiple components together
(service + repository, API endpoints). Use real database or test database. E2E
tests
test complete user workflows in browser. Testing pyramid: many unit tests (70%), some integration (20%), few E2E
(10%). Use Supertest for API testing. Playwright/Cypress for browser automation. E2E tests are slow, expensive -
focus on critical paths. Include visual regression, mobile, performance testing.
Section 24 Summary: Testing and Code Quality
- Unit Testing: Test individual units in isolation, follow AAA pattern (Arrange, Act, Assert), fast and repeatable
- Test Structure: Organize with describe/it blocks, setup with beforeEach/afterEach, descriptive test names
- Mocking: Use mocks (verify interactions), stubs (fixed responses), spies (track calls), jest.fn() for mocks
- Mock Assertions: toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes, mockReturnValue, mockResolvedValue
- TDD Cycle: Red (write failing test), Green (make it pass), Refactor (improve code while tests pass)
- TDD Benefits: Better design, documentation, confidence, regression prevention, focus on requirements
- Code Coverage: Measure line, branch, function, statement coverage - aim for 80-90%, branch coverage most thorough
- Quality Metrics: Cyclomatic complexity, maintainability index, code duplication, technical debt
- Assertions: toBe (===), toEqual (deep), toMatch (regex), toContain, toThrow, toBeGreaterThan, toHaveProperty
- Test Utilities: Faker for test data, custom matchers with expect.extend, snapshot testing, asymmetric matchers
- Integration Tests: Test multiple components together (service + repository, API endpoints), use test database
- E2E Testing: Test complete workflows with Playwright/Cypress, testing pyramid (70% unit, 20% integration, 10% E2E)
25. Security and Best Practices
25.1 Input Validation and Sanitization
Common Input Threats
| Threat | Description | Prevention |
|---|---|---|
| SQL Injection | Malicious SQL in input | Parameterized queries, ORM |
| XSS (Cross-Site Scripting) | Malicious scripts in content | Escape output, CSP headers |
| Command Injection | OS commands in input | Avoid shell execution, whitelist |
| Path Traversal | Access unauthorized files | Validate paths, use basedir |
| LDAP Injection | Malicious LDAP queries | Escape special chars, whitelist |
Validation Strategies
| Strategy | When to Use | Example |
|---|---|---|
| Whitelist | Known valid values | Enum values, file extensions |
| Blacklist | Last resort only | Block specific patterns |
| Type Checking | Enforce data types | typeof, instanceof, schema |
| Length Limits | Prevent overflow/DoS | Max string/array length |
| Format Validation | Structured data | Email, URL, phone regex |
Sanitization Techniques
| Technique | Purpose | Library |
|---|---|---|
| HTML Escaping | Prevent XSS | DOMPurify, he |
| URL Encoding | Safe URL parameters | encodeURIComponent() |
| SQL Escaping | Prevent SQL injection | ORM, prepared statements |
| Trim/Normalize | Consistent format | trim(), normalize() |
Example: Input validation and sanitization
// Email validation
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Strong password validation
function isStrongPassword(password) {
// At least 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char
const minLength = password.length >= 8;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return minLength && hasUpper && hasLower && hasNumber && hasSpecial;
}
// Comprehensive input validator
class InputValidator {
static validateString(value, options = {}) {
const {
minLength = 0,
maxLength = Infinity,
pattern = null,
allowEmpty = false
} = options;
if (!value && !allowEmpty) {
throw new Error('Value is required');
}
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
if (value.length < minLength) {
throw new Error(`Value must be at least ${minLength} characters`);
}
if (value.length > maxLength) {
throw new Error(`Value must not exceed ${maxLength} characters`);
}
if (pattern && !pattern.test(value)) {
throw new Error('Value does not match required pattern');
}
return true;
}
static validateNumber(value, options = {}) {
const {
min = -Infinity,
max = Infinity,
integer = false
} = options;
const num = Number(value);
if (isNaN(num)) {
throw new Error('Value must be a number');
}
if (integer && !Number.isInteger(num)) {
throw new Error('Value must be an integer');
}
if (num < min) {
throw new Error(`Value must be at least ${min}`);
}
if (num > max) {
throw new Error(`Value must not exceed ${max}`);
}
return true;
}
static validateEnum(value, allowedValues) {
if (!allowedValues.includes(value)) {
throw new Error(
`Value must be one of: ${allowedValues.join(', ')}`
);
}
return true;
}
static validateArray(value, options = {}) {
const {
minLength = 0,
maxLength = Infinity,
itemValidator = null
} = options;
if (!Array.isArray(value)) {
throw new Error('Value must be an array');
}
if (value.length < minLength) {
throw new Error(`Array must have at least ${minLength} items`);
}
if (value.length > maxLength) {
throw new Error(`Array must not exceed ${maxLength} items`);
}
if (itemValidator) {
value.forEach((item, index) => {
try {
itemValidator(item);
} catch (error) {
throw new Error(`Item at index ${index}: ${error.message}`);
}
});
}
return true;
}
}
// Usage examples
try {
InputValidator.validateString('hello', {minLength: 3, maxLength: 10});
InputValidator.validateNumber(25, {min: 0, max: 100});
InputValidator.validateEnum('active', ['active', 'inactive', 'pending']);
console.log('All validations passed');
} catch (error) {
console.error('Validation error:', error.message);
}
// HTML escaping to prevent XSS
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// Safe HTML rendering
function safeRender(userInput) {
const escaped = escapeHtml(userInput);
document.getElementById('output').textContent = escaped;
// or use textContent instead of innerHTML
}
// URL parameter sanitization
function sanitizeUrlParam(param) {
return encodeURIComponent(param);
}
// Build safe URL
function buildUrl(base, params) {
const queryString = Object.entries(params)
.map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
)
.join('&');
return `${base}?${queryString}`;
}
const url = buildUrl('https://api.example.com/search', {
q: 'user input <script>',
page: 1
});
// Result: https://api.example.com/search?q=user%20input%20%3Cscript%3E&page=1
// Filename sanitization (prevent path traversal)
function sanitizeFilename(filename) {
// Remove path separators and null bytes
return filename
.replace(/[\/\\]/g, '')
.replace(/\0/g, '')
.replace(/\.\./g, '')
.slice(0, 255); // Limit length
}
// Path traversal prevention
function safePath(userPath, baseDir) {
const path = require('path');
const resolvedPath = path.resolve(baseDir, userPath);
// Ensure resolved path is within baseDir
if (!resolvedPath.startsWith(path.resolve(baseDir))) {
throw new Error('Path traversal detected');
}
return resolvedPath;
}
// Schema validation with custom validator
const userSchema = {
username: {
type: 'string',
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/
},
email: {
type: 'string',
validator: isValidEmail
},
age: {
type: 'number',
min: 18,
max: 120,
integer: true
},
role: {
type: 'enum',
values: ['user', 'admin', 'moderator']
}
};
function validateSchema(data, schema) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = data[field];
try {
if (rules.type === 'string') {
InputValidator.validateString(value, rules);
} else if (rules.type === 'number') {
InputValidator.validateNumber(value, rules);
} else if (rules.type === 'enum') {
InputValidator.validateEnum(value, rules.values);
}
if (rules.validator && !rules.validator(value)) {
errors.push(`${field}: Custom validation failed`);
}
} catch (error) {
errors.push(`${field}: ${error.message}`);
}
}
return {
valid: errors.length === 0,
errors
};
}
// Usage
const userData = {
username: 'john_doe',
email: 'john@example.com',
age: 25,
role: 'user'
};
const result = validateSchema(userData, userSchema);
if (result.valid) {
console.log('User data is valid');
} else {
console.error('Validation errors:', result.errors);
}
// SQL injection prevention (using parameterized queries)
// BAD: Vulnerable to SQL injection
function getUserBad(userId) {
const query = `SELECT * FROM users WHERE id = ${userId}`;
// If userId is "1 OR 1=1", returns all users!
return db.query(query);
}
// GOOD: Parameterized query
function getUserGood(userId) {
const query = 'SELECT * FROM users WHERE id = ?';
return db.query(query, [userId]);
}
// ORM approach (safer)
function getUserORM(userId) {
return User.findById(userId);
}
// Command injection prevention
// BAD: Vulnerable to command injection
function processBad(filename) {
const exec = require('child_process').exec;
exec(`cat ${filename}`, (error, stdout) => {
console.log(stdout);
});
// If filename is "file.txt; rm -rf /", disaster!
}
// GOOD: Use array syntax, avoid shell
function processGood(filename) {
const {execFile} = require('child_process');
execFile('cat', [filename], (error, stdout) => {
console.log(stdout);
});
}
// Rate limiting for API endpoints
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
isAllowed(identifier) {
const now = Date.now();
const userRequests = this.requests.get(identifier) || [];
// Remove old requests outside window
const validRequests = userRequests.filter(
time => now - time < this.windowMs
);
if (validRequests.length >= this.maxRequests) {
return false;
}
validRequests.push(now);
this.requests.set(identifier, validRequests);
return true;
}
}
// Usage
const limiter = new RateLimiter(100, 60000); // 100 requests per minute
function handleRequest(req, res) {
const userId = req.userId;
if (!limiter.isAllowed(userId)) {
return res.status(429).json({error: 'Too many requests'});
}
// Process request
}
// Content Security Policy (CSP) nonce generation
function generateNonce() {
const crypto = require('crypto');
return crypto.randomBytes(16).toString('base64');
}
Key Points: Always validate input on server side (client-side
validation is for UX only). Use whitelist validation when possible. Escape HTML
to prevent XSS. Use parameterized queries to prevent SQL injection. Sanitize filenames to prevent path
traversal. Implement rate limiting to prevent DoS. Never trust user input. Validate type, length, format, and
range. Use established libraries for validation (Joi, Yup, validator.js).
25.2 XSS Prevention and Content Security Policy
XSS Attack Types
| Type | Description | Example |
|---|---|---|
| Reflected XSS | Script in URL/input reflected immediately | ?q=<script>alert(1)</script> |
| Stored XSS | Script stored in database, served to users | Comment with malicious script |
| DOM-based XSS | Client-side script manipulation | document.write(location.hash) |
| Mutation XSS | Browser mutates sanitized HTML | Nested tags, encoding tricks |
XSS Prevention Techniques
| Technique | How It Works | Implementation |
|---|---|---|
| Output Encoding | Escape HTML entities | < > & " |
| Use textContent | Automatically escapes content | element.textContent = data |
| Avoid innerHTML | Prevents script execution | Use DOM methods instead |
| CSP Headers | Restrict script sources | Content-Security-Policy header |
| DOMPurify | Sanitize HTML safely | Library for HTML cleaning |
CSP Directives
| Directive | Purpose | Example Value |
|---|---|---|
| default-src | Fallback for all sources | 'self' |
| script-src | JavaScript sources | 'self' 'nonce-xyz123' |
| style-src | CSS sources | 'self' 'unsafe-inline' |
| img-src | Image sources | 'self' data: https: |
| connect-src | XHR/WebSocket/EventSource | 'self' https://api.example.com |
| font-src | Font sources | 'self' https://fonts.gstatic.com |
| frame-ancestors | Who can embed in iframe | 'none' or 'self' |
Example: XSS prevention
// XSS vulnerable code examples
// BAD: innerHTML with user input
function displayUsernameBad(username) {
document.getElementById('greeting').innerHTML =
`<h1>Welcome, ${username}!</h1>`;
// If username is "<img src=x onerror=alert('XSS')>", XSS occurs!
}
// GOOD: Use textContent
function displayUsernameGood(username) {
const h1 = document.createElement('h1');
h1.textContent = `Welcome, ${username}!`;
document.getElementById('greeting').appendChild(h1);
}
// BAD: eval with user input
function executeBad(userCode) {
eval(userCode); // NEVER DO THIS!
}
// GOOD: Use safe alternatives
function executeGood(userCode) {
// If you must parse JSON
const data = JSON.parse(userCode);
// For calculations, use Function constructor with validation
// But still be very careful!
}
// BAD: Direct DOM manipulation from URL
function displayFromUrlBad() {
const params = new URLSearchParams(window.location.search);
document.getElementById('content').innerHTML = params.get('message');
}
// GOOD: Sanitize before display
function displayFromUrlGood() {
const params = new URLSearchParams(window.location.search);
const message = params.get('message');
document.getElementById('content').textContent = message;
}
// HTML escaping function
function escapeHtml2(unsafe) {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// JavaScript escaping (for embedding in <script> tags)
function escapeJs(unsafe) {
return unsafe
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/</g, '\\x3c')
.replace(/>/g, '\\x3e');
}
// URL encoding
function escapeUrl(unsafe) {
return encodeURIComponent(unsafe);
}
// Context-aware escaping
function safeDomInsert(element, content, context) {
switch (context) {
case 'text':
element.textContent = content;
break;
case 'attribute':
element.setAttribute('data-value', escapeHtml2(content));
break;
case 'url':
element.href = escapeUrl(content);
break;
case 'html':
// Use DOMPurify if you must allow HTML
// element.innerHTML = DOMPurify.sanitize(content);
break;
default:
throw new Error('Invalid context');
}
}
// DOMPurify usage (safe HTML sanitization)
// import DOMPurify from 'dompurify';
function renderSafeHtml(userHtml) {
const clean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
document.getElementById('content').innerHTML = clean;
}
// CSP Implementation
// Setting CSP via HTTP header (server-side)
/*
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-xyz123' https://trusted.cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
*/
// Setting CSP via meta tag
const cspMeta = document.createElement('meta');
cspMeta.httpEquiv = 'Content-Security-Policy';
cspMeta.content = "default-src 'self'; script-src 'self' 'unsafe-inline'";
document.head.appendChild(cspMeta);
// Nonce-based CSP
// Server generates nonce
function generateCspNonce() {
const crypto = require('crypto');
return crypto.randomBytes(16).toString('base64');
}
// In Express.js middleware
function cspMiddleware(req, res, next) {
const nonce = generateCspNonce();
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'`
);
next();
}
// In HTML template
// <script nonce="<%= nonce %>">
// console.log('This script is allowed');
// </script>
// CSP violation reporting
const cspWithReporting = `
default-src 'self';
script-src 'self';
report-uri /csp-violation-report-endpoint;
report-to csp-endpoint
`;
// Report-To header
const reportTo = JSON.stringify({
group: 'csp-endpoint',
max_age: 10886400,
endpoints: [{url: 'https://example.com/csp-reports'}]
});
// Handle CSP violation reports
function handleCspReport(req, res) {
const report = req.body;
console.log('CSP Violation:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number']
});
res.status(204).send();
}
// Trusted Types API (modern XSS prevention)
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (string) => {
// Sanitize string
return DOMPurify.sanitize(string);
},
createScriptURL: (string) => {
// Validate script URL
const url = new URL(string, window.location.href);
if (url.origin === window.location.origin) {
return string;
}
throw new TypeError('Invalid script URL');
}
});
// Usage with Trusted Types
const html = policy.createHTML('<b>Safe HTML</b>');
element.innerHTML = html; // No XSS!
}
// XSS protection headers
function setSecurityHeaders(res) {
// XSS Protection (legacy browsers)
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Frame options (clickjacking prevention)
res.setHeader('X-Frame-Options', 'DENY');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
// Safe JSON embedding in HTML
function safeJsonEmbed(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
}
// In HTML:
// <script>
// const data = <%= safeJsonEmbed(userData) %>;
// </script>
// Safe event handler binding
// BAD: Inline event handlers
// <button onclick="userFunction()">Click</button>
// GOOD: addEventListener
const button = document.getElementById('myButton');
button.addEventListener('click', function(e) {
e.preventDefault();
// Safe handler
});
// Sanitize URLs to prevent javascript: protocol
function isSafeUrl(url) {
try {
const parsed = new URL(url, window.location.href);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}
function setSafeHref(element, url) {
if (isSafeUrl(url)) {
element.href = url;
} else {
console.error('Unsafe URL blocked:', url);
element.href = '#';
}
}
Key Points: Never use innerHTML with user input - use
textContent or DOM methods. Never use eval() or Function() with user input.
Implement CSP headers to restrict script sources. Use nonce or hash for inline scripts. DOMPurify for safe HTML
sanitization. Trusted Types API for modern browsers. Context-aware escaping (HTML, JavaScript, URL, CSS).
Validate URLs to prevent javascript: protocol. Report CSP violations for monitoring.
25.3 Secure Coding Practices and Code Review
OWASP Top 10 Web Vulnerabilities
| Vulnerability | Risk | Prevention |
|---|---|---|
| Broken Access Control | Unauthorized access to resources | Enforce authorization checks |
| Cryptographic Failures | Sensitive data exposure | Encrypt data at rest and transit |
| Injection | SQL/Command/LDAP injection | Parameterized queries, validation |
| Insecure Design | Missing security controls | Threat modeling, secure design |
| Security Misconfiguration | Default configs, verbose errors | Harden configs, disable defaults |
| Vulnerable Components | Outdated libraries with CVEs | Regular updates, dependency scanning |
| Authentication Failures | Weak auth, session management | Strong passwords, MFA, secure sessions |
| Data Integrity Failures | Insecure deserialization | Validate serialized data, use safe formats |
| Logging Failures | Insufficient monitoring | Comprehensive logging, alerting |
| SSRF | Server-side request forgery | Whitelist URLs, validate destinations |
Secure Coding Principles
| Principle | Description | Example |
|---|---|---|
| Defense in Depth | Multiple security layers | Input validation + CSP + WAF |
| Least Privilege | Minimum necessary permissions | Read-only DB user for queries |
| Fail Securely | Deny by default on errors | Default to unauthorized |
| Complete Mediation | Check every access | Validate auth on each request |
| Open Design | Security through design, not obscurity | Public algorithms, secret keys |
| Separation of Duties | Split critical operations | Approver ≠ Requester |
Code Review Security Checklist
| Category | What to Check | Red Flags |
|---|---|---|
| Input Validation | All inputs validated and sanitized | Direct use of user input |
| Authentication | Proper auth checks, session management | Missing auth, weak passwords |
| Authorization | Access control enforced | Missing permission checks |
| Cryptography | Strong algorithms, proper key management | Hardcoded secrets, weak ciphers |
| Error Handling | No sensitive info in errors | Stack traces to users |
| Logging | Security events logged | Passwords in logs |
| Dependencies | Up-to-date, no known vulnerabilities | Outdated libraries |
Example: Secure coding practices
// Secure configuration management
// BAD: Hardcoded secrets
const API_KEY = 'sk_live_abc123xyz'; // NEVER DO THIS!
const DB_PASSWORD = 'mypassword123';
// GOOD: Environment variables
const API_KEY2 = process.env.API_KEY;
const DB_PASSWORD2 = process.env.DB_PASSWORD;
// BETTER: Use a secrets manager
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
async function getSecret(secretName) {
const [version] = await client.accessSecretVersion({
name: secretName
});
return version.payload.data.toString();
}
// Error handling - don't leak sensitive info
// BAD: Verbose error messages
function handleErrorBad(error, res) {
res.status(500).json({
error: error.message,
stack: error.stack, // Exposes internal structure!
query: error.sql // Exposes SQL queries!
});
}
// GOOD: Generic error messages
function handleErrorGood(error, res) {
console.error('Internal error:', error); // Log for debugging
res.status(500).json({
error: 'An error occurred. Please try again later.',
requestId: generateRequestId() // For support tracking
});
}
// Secure session management
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({client: redisClient}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
},
name: 'sessionId' // Don't use default name
}));
// Authorization middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({error: 'Unauthorized'});
}
next();
}
function requireRole(role) {
return async (req, res, next) => {
const user = await User.findById(req.session.userId);
if (!user || user.role !== role) {
return res.status(403).json({error: 'Forbidden'});
}
next();
};
}
// Usage
app.get('/admin', requireAuth, requireRole('admin'), (req, res) => {
// Admin-only endpoint
});
// Secure file uploads
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
// Generate safe filename
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
},
fileFilter: (req, file, cb) => {
// Whitelist file types
const allowedTypes = /jpeg|jpg|png|pdf/;
const extname = allowedTypes.test(
path.extname(file.originalname).toLowerCase()
);
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// Resource access control
class ResourceAccessControl {
static canAccess(user, resource, action) {
// Check if user owns resource
if (resource.userId === user.id) {
return true;
}
// Check role-based permissions
const permissions = {
admin: ['read', 'write', 'delete'],
moderator: ['read', 'write'],
user: ['read']
};
const userPermissions = permissions[user.role] || [];
return userPermissions.includes(action);
}
}
// Usage in endpoint
app.delete('/posts/:id', requireAuth, async (req, res) => {
const post = await Post.findById(req.params.id);
const user = await User.findById(req.session.userId);
if (!ResourceAccessControl.canAccess(user, post, 'delete')) {
return res.status(403).json({error: 'Forbidden'});
}
await post.delete();
res.json({success: true});
});
// Secure random number generation
// BAD: Predictable
const badRandom = Math.random();
// GOOD: Cryptographically secure
const crypto = require('crypto');
const goodRandom = crypto.randomBytes(32).toString('hex');
// Generate secure token
function generateSecureToken() {
return crypto.randomBytes(32).toString('base64url');
}
// Time-constant string comparison (prevent timing attacks)
// BAD: Vulnerable to timing attacks
function compareTokenBad(a, b) {
return a === b; // Early return leaks info
}
// GOOD: Constant-time comparison
function compareTokenGood(a, b) {
return crypto.timingSafeEqual(
Buffer.from(a),
Buffer.from(b)
);
}
// Dependency scanning
// package.json scripts
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"audit:check": "npm audit --audit-level=high"
}
}
// Automated security checks in CI/CD
// .github/workflows/security.yml
/*
name: Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run npm audit
run: npm audit --audit-level=moderate
- name: Run Snyk
run: npx snyk test
*/
// Secure API key storage
class ApiKeyManager {
constructor() {
this.keys = new Map();
}
async storeKey(userId, apiKey) {
const crypto = require('crypto');
// Hash the key (don't store plain text)
const hash = crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
await db.apiKeys.insert({
userId,
keyHash: hash,
createdAt: new Date()
});
// Return key once to user, then forget it
return apiKey;
}
async validateKey(apiKey) {
const hash = crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
const record = await db.apiKeys.findOne({keyHash: hash});
return !!record;
}
}
// Logging security events
class SecurityLogger {
static logAuthSuccess(userId, ip) {
logger.info('Auth success', {userId, ip, event: 'LOGIN'});
}
static logAuthFailure(username, ip, reason) {
logger.warn('Auth failure', {username, ip, reason, event: 'LOGIN_FAIL'});
}
static logAccessDenied(userId, resource, action) {
logger.warn('Access denied', {
userId,
resource,
action,
event: 'ACCESS_DENIED'
});
}
static logSensitiveOperation(userId, operation, details) {
logger.info('Sensitive operation', {
userId,
operation,
details,
event: 'SENSITIVE_OP'
});
}
}
// Never log sensitive data
// BAD
logger.info('User login', {password: userPassword}); // NEVER!
// GOOD
logger.info('User login', {userId: user.id, timestamp: new Date()});
// Secure headers middleware
function securityHeaders(req, res, next) {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// HTTPS only
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(), camera=()');
next();
}
app.use(securityHeaders);
Key Points: Never hardcode secrets - use environment variables
or secrets manager. Don't expose detailed errors to users. Implement proper authorization checks. Use secure
session management (httpOnly, secure, sameSite cookies). Whitelist file uploads by type and size. Use
cryptographically secure random for tokens. Constant-time comparison for sensitive strings. Run npm audit
regularly. Log security events without sensitive data. Apply security headers. Follow OWASP Top 10. Code review
for security issues. Defense in depth approach.
25.4 Data Encryption and Hashing
Encryption vs Hashing
| Feature | Encryption | Hashing |
|---|---|---|
| Reversible | Yes (with key) | No (one-way) |
| Purpose | Protect data confidentiality | Verify data integrity |
| Use Case | Store credit cards, encrypt messages | Store passwords, checksums |
| Output | Variable length ciphertext | Fixed length hash digest |
| Key Required | Yes | No (salt optional) |
Encryption Algorithms
| Algorithm | Type | Key Size | Use Case |
|---|---|---|---|
| AES | Symmetric | 128, 192, 256 bits | General purpose encryption |
| RSA | Asymmetric | 2048, 4096 bits | Key exchange, digital signatures |
| ChaCha20 | Symmetric stream | 256 bits | Mobile devices, TLS |
| ECC | Asymmetric | 256, 384 bits | Smaller keys, IoT devices |
Hashing Algorithms
| Algorithm | Output Size | Status | Use Case |
|---|---|---|---|
| bcrypt | 60 chars | ✓ Secure | Password hashing (recommended) |
| scrypt | Variable | ✓ Secure | Password hashing, key derivation |
| Argon2 | Variable | ✓ Secure (newest) | Password hashing (best) |
| SHA-256 | 256 bits | ✓ Secure | Checksums, NOT passwords |
| SHA-1 | 160 bits | ✗ Broken | Deprecated |
| MD5 | 128 bits | ✗ Broken | Never use |
Example: Encryption and hashing
const crypto = require('crypto');
// === PASSWORD HASHING ===
// BAD: Plain MD5 (broken, too fast)
function hashPasswordBad(password) {
return crypto.createHash('md5').update(password).digest('hex');
// Vulnerable to rainbow tables and brute force
}
// GOOD: bcrypt with salt
const bcrypt = require('bcrypt');
async function hashPassword(password) {
const saltRounds = 12; // Higher = more secure but slower
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Usage
async function registerUser(username, password) {
const passwordHash = await hashPassword(password);
await db.users.insert({
username,
passwordHash, // Store hash, never plain password
createdAt: new Date()
});
}
async function loginUser(username, password) {
const user = await db.users.findOne({username});
if (!user) {
return false;
}
const isValid = await verifyPassword(password, user.passwordHash);
return isValid ? user : null;
}
// === SYMMETRIC ENCRYPTION (AES) ===
// Encrypt data with AES-256-GCM
function encrypt(plaintext, key) {
// Generate random IV (initialization vector)
const iv = crypto.randomBytes(16);
// Create cipher
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// Encrypt
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get authentication tag
const authTag = cipher.getAuthTag();
// Return IV + authTag + encrypted data
return {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
encrypted: encrypted
};
}
function decrypt(encryptedData, key) {
// Create decipher
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encryptedData.iv, 'hex')
);
// Set authentication tag
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
// Decrypt
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Usage
const encryptionKey = crypto.randomBytes(32); // 256 bits
const message = 'Sensitive data';
const encrypted = encrypt(message, encryptionKey);
console.log('Encrypted:', encrypted);
const decrypted = decrypt(encrypted, encryptionKey);
console.log('Decrypted:', decrypted); // 'Sensitive data'
// === KEY DERIVATION ===
// Derive encryption key from password using PBKDF2
function deriveKey(password, salt) {
return crypto.pbkdf2Sync(
password,
salt,
100000, // iterations
32, // key length (256 bits)
'sha256' // hash algorithm
);
}
// Encrypt with password
function encryptWithPassword(plaintext, password) {
const salt = crypto.randomBytes(16);
const key = deriveKey(password, salt);
const encrypted = encrypt(plaintext, key);
return {
salt: salt.toString('hex'),
...encrypted
};
}
function decryptWithPassword(encryptedData, password) {
const salt = Buffer.from(encryptedData.salt, 'hex');
const key = deriveKey(password, salt);
return decrypt({
iv: encryptedData.iv,
authTag: encryptedData.authTag,
encrypted: encryptedData.encrypted
}, key);
}
// === ASYMMETRIC ENCRYPTION (RSA) ===
// Generate RSA key pair
function generateKeyPair() {
const {publicKey, privateKey} = crypto.generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: process.env.KEY_PASSPHRASE
}
});
return {publicKey, privateKey};
}
// Encrypt with public key
function rsaEncrypt(plaintext, publicKey) {
const buffer = Buffer.from(plaintext, 'utf8');
const encrypted = crypto.publicEncrypt(publicKey, buffer);
return encrypted.toString('base64');
}
// Decrypt with private key
function rsaDecrypt(encrypted, privateKey, passphrase) {
const buffer = Buffer.from(encrypted, 'base64');
const decrypted = crypto.privateDecrypt(
{
key: privateKey,
passphrase: passphrase
},
buffer
);
return decrypted.toString('utf8');
}
// Digital signature
function sign(message, privateKey, passphrase) {
const signer = crypto.createSign('sha256');
signer.update(message);
signer.end();
return signer.sign({
key: privateKey,
passphrase: passphrase
}, 'base64');
}
function verify(message, signature, publicKey) {
const verifier = crypto.createVerify('sha256');
verifier.update(message);
verifier.end();
return verifier.verify(publicKey, signature, 'base64');
}
// === HMAC (Hash-based Message Authentication Code) ===
function generateHmac(message, secret) {
return crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
}
function verifyHmac(message, hmac, secret) {
const calculated = generateHmac(message, secret);
return crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(calculated)
);
}
// Use for API request signing
function signRequest(payload, apiSecret) {
const timestamp = Date.now();
const message = `${timestamp}.${JSON.stringify(payload)}`;
const signature = generateHmac(message, apiSecret);
return {
timestamp,
payload,
signature
};
}
function verifyRequest(request, apiSecret, maxAge = 300000) {
const {timestamp, payload, signature} = request;
// Check timestamp (prevent replay attacks)
if (Date.now() - timestamp > maxAge) {
return false;
}
// Verify signature
const message = `${timestamp}.${JSON.stringify(payload)}`;
return verifyHmac(message, signature, apiSecret);
}
// === SECURE TOKEN GENERATION ===
function generateToken(length = 32) {
return crypto.randomBytes(length).toString('base64url');
}
// JWT-like token (simplified)
function createToken(payload, secret, expiresIn = 3600) {
const header = {alg: 'HS256', typ: 'JWT'};
const claims = {
...payload,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + expiresIn
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
const encodedPayload = Buffer.from(JSON.stringify(claims)).toString('base64url');
const signature = generateHmac(`${encodedHeader}.${encodedPayload}`, secret);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
function verifyToken(token, secret) {
const [encodedHeader, encodedPayload, signature] = token.split('.');
// Verify signature
const expectedSignature = generateHmac(
`${encodedHeader}.${encodedPayload}`,
secret
);
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
throw new Error('Invalid signature');
}
// Decode and check expiration
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString());
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expired');
}
return payload;
}
// === ENCRYPTING DATA AT REST ===
class EncryptedStorage {
constructor(key) {
this.key = key;
}
set(key, value) {
const encrypted = encrypt(JSON.stringify(value), this.key);
localStorage.setItem(key, JSON.stringify(encrypted));
}
get(key) {
const stored = localStorage.getItem(key);
if (!stored) return null;
const encrypted = JSON.parse(stored);
const decrypted = decrypt(encrypted, this.key);
return JSON.parse(decrypted);
}
remove(key) {
localStorage.removeItem(key);
}
}
// Usage
const masterKey = deriveKey(userPassword, userSalt);
const storage = new EncryptedStorage(masterKey);
storage.set('sensitiveData', {ssn: '123-45-6789', account: '9876543210'});
const data = storage.get('sensitiveData');
// === WEB CRYPTO API (Browser) ===
async function generateAesKey() {
return await window.crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}
async function encryptBrowser(plaintext, key) {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encoded
);
return {
iv: Array.from(iv),
ciphertext: Array.from(new Uint8Array(ciphertext))
};
}
async function decryptBrowser(encrypted, key) {
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(encrypted.iv)
},
key,
new Uint8Array(encrypted.ciphertext)
);
return new TextDecoder().decode(decrypted);
}
// === KEY MANAGEMENT BEST PRACTICES ===
// Store keys securely
// - Use environment variables
// - Use a secrets manager (AWS Secrets Manager, HashiCorp Vault)
// - Never commit keys to version control
// - Rotate keys regularly
// - Use different keys for different environments
// Key rotation example
class KeyRotation {
constructor() {
this.currentKey = null;
this.oldKeys = [];
}
rotateKey() {
if (this.currentKey) {
this.oldKeys.push(this.currentKey);
}
this.currentKey = crypto.randomBytes(32);
// Keep only last 3 old keys
if (this.oldKeys.length > 3) {
this.oldKeys.shift();
}
}
encrypt(data) {
return encrypt(data, this.currentKey);
}
decrypt(encryptedData) {
// Try current key first
try {
return decrypt(encryptedData, this.currentKey);
} catch (error) {
// Try old keys
for (const oldKey of this.oldKeys) {
try {
return decrypt(encryptedData, oldKey);
} catch {
continue;
}
}
throw new Error('Failed to decrypt with any key');
}
}
}
Key Points: Use bcrypt/Argon2 for password hashing, never plain
MD5/SHA. Use AES-256-GCM for symmetric encryption. Use RSA/ECC for asymmetric
encryption. Always use random IV for each encryption. HMAC for message authentication. Derive keys from
passwords
with PBKDF2/scrypt. Store keys securely (never hardcode). Rotate keys regularly. Use Web Crypto API in browsers.
Encryption protects confidentiality, hashing verifies integrity. Salt passwords to prevent rainbow tables.
25.5 Authentication and Authorization Patterns
Authentication Methods
| Method | How It Works | Use Case |
|---|---|---|
| Session-based | Server stores session, client has session ID | Traditional web apps |
| Token-based (JWT) | Stateless token with claims | APIs, SPAs, mobile apps |
| OAuth 2.0 | Delegated authorization | Third-party login |
| OpenID Connect | OAuth 2.0 + identity layer | SSO, enterprise auth |
| API Keys | Static key per client | Server-to-server |
| MFA/2FA | Multiple authentication factors | High security accounts |
Authorization Models
| Model | Description | Example |
|---|---|---|
| RBAC | Role-Based Access Control | Admin, User, Guest roles |
| ABAC | Attribute-Based Access Control | Rules based on attributes |
| ACL | Access Control List | Per-resource permissions |
| PBAC | Policy-Based Access Control | Complex business rules |
JWT Structure
| Part | Content | Purpose |
|---|---|---|
| Header | Algorithm, token type | Specify signing method |
| Payload | Claims (user ID, exp, etc.) | Carry user information |
| Signature | HMAC or RSA signature | Verify token integrity |
Example: Authentication and authorization
// === JWT AUTHENTICATION ===
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
// Generate JWT token
function generateJWT(user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
const token = jwt.sign(
payload,
JWT_SECRET,
{
expiresIn: '1h',
issuer: 'myapp.com',
audience: 'myapp-users'
}
);
return token;
}
// Verify JWT token
function verifyJWT(token) {
try {
const decoded = jwt.verify(token, JWT_SECRET, {
issuer: 'myapp.com',
audience: 'myapp-users'
});
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
// Authentication middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({error: 'No token provided'});
}
const token = authHeader.substring(7);
try {
const decoded = verifyJWT(token);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({error: error.message});
}
}
// === REFRESH TOKEN PATTERN ===
function generateTokenPair(user) {
const accessToken = jwt.sign(
{userId: user.id, email: user.email, role: user.role},
JWT_SECRET,
{expiresIn: '15m'} // Short-lived
);
const refreshToken = jwt.sign(
{userId: user.id, type: 'refresh'},
process.env.REFRESH_TOKEN_SECRET,
{expiresIn: '7d'} // Long-lived
);
return {accessToken, refreshToken};
}
async function refreshAccessToken(refreshToken) {
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
if (decoded.type !== 'refresh') {
throw new Error('Invalid token type');
}
// Check if refresh token is revoked
const isRevoked = await checkTokenRevocation(refreshToken);
if (isRevoked) {
throw new Error('Token revoked');
}
// Generate new access token
const user = await User.findById(decoded.userId);
const accessToken = generateJWT(user);
return accessToken;
} catch (error) {
throw new Error('Invalid refresh token');
}
}
// === ROLE-BASED ACCESS CONTROL (RBAC) ===
const ROLES = {
ADMIN: 'admin',
MODERATOR: 'moderator',
USER: 'user',
GUEST: 'guest'
};
const PERMISSIONS = {
READ_POSTS: 'read:posts',
WRITE_POSTS: 'write:posts',
DELETE_POSTS: 'delete:posts',
MANAGE_USERS: 'manage:users',
VIEW_ANALYTICS: 'view:analytics'
};
const ROLE_PERMISSIONS = {
[ROLES.ADMIN]: [
PERMISSIONS.READ_POSTS,
PERMISSIONS.WRITE_POSTS,
PERMISSIONS.DELETE_POSTS,
PERMISSIONS.MANAGE_USERS,
PERMISSIONS.VIEW_ANALYTICS
],
[ROLES.MODERATOR]: [
PERMISSIONS.READ_POSTS,
PERMISSIONS.WRITE_POSTS,
PERMISSIONS.DELETE_POSTS
],
[ROLES.USER]: [
PERMISSIONS.READ_POSTS,
PERMISSIONS.WRITE_POSTS
],
[ROLES.GUEST]: [
PERMISSIONS.READ_POSTS
]
};
function hasPermission(userRole, permission) {
const permissions = ROLE_PERMISSIONS[userRole] || [];
return permissions.includes(permission);
}
// Authorization middleware
function requirePermission(permission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({error: 'Unauthorized'});
}
if (!hasPermission(req.user.role, permission)) {
return res.status(403).json({error: 'Forbidden'});
}
next();
};
}
// Usage
app.delete('/posts/:id',
authenticate,
requirePermission(PERMISSIONS.DELETE_POSTS),
async (req, res) => {
// Delete post
}
);
// === ATTRIBUTE-BASED ACCESS CONTROL (ABAC) ===
class AbacPolicy {
static canAccess(subject, resource, action, context) {
// Subject: who is trying to access (user)
// Resource: what is being accessed (document, post, etc.)
// Action: what operation (read, write, delete)
// Context: environmental factors (time, location, etc.)
// Rule 1: Users can read their own posts
if (action === 'read' && resource.authorId === subject.userId) {
return true;
}
// Rule 2: Admins can do anything
if (subject.role === 'admin') {
return true;
}
// Rule 3: Users can edit posts during business hours only
if (action === 'write' && resource.authorId === subject.userId) {
const hour = new Date().getHours();
return hour >= 9 && hour < 17; // 9 AM - 5 PM
}
// Rule 4: Moderators can delete flagged posts
if (action === 'delete' && subject.role === 'moderator' && resource.flagged) {
return true;
}
// Rule 5: Premium users can access premium content
if (resource.type === 'premium' && subject.subscription === 'premium') {
return true;
}
return false;
}
}
// Usage
app.put('/posts/:id', authenticate, async (req, res) => {
const post = await Post.findById(req.params.id);
const context = {timestamp: Date.now()};
if (!AbacPolicy.canAccess(req.user, post, 'write', context)) {
return res.status(403).json({error: 'Forbidden'});
}
// Update post
});
// === OAUTH 2.0 IMPLEMENTATION ===
// Authorization code flow
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: 'http://localhost:3000/auth/google/callback',
response_type: 'code',
scope: 'openid email profile',
state: generateSecureToken() // CSRF protection
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
app.get('/auth/google/callback', async (req, res) => {
const {code, state} = req.query;
// Verify state (CSRF protection)
if (!verifyState(state)) {
return res.status(400).json({error: 'Invalid state'});
}
// Exchange code for token
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: 'http://localhost:3000/auth/google/callback',
grant_type: 'authorization_code'
})
});
const tokens = await response.json();
// Get user info
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {Authorization: `Bearer ${tokens.access_token}`}
});
const googleUser = await userResponse.json();
// Create or update user in database
let user = await User.findOne({googleId: googleUser.id});
if (!user) {
user = await User.create({
googleId: googleUser.id,
email: googleUser.email,
name: googleUser.name,
avatar: googleUser.picture
});
}
// Generate JWT for our app
const appToken = generateJWT(user);
res.json({token: appToken});
});
// === MULTI-FACTOR AUTHENTICATION (MFA) ===
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Setup MFA
async function setupMFA(user) {
const secret = speakeasy.generateSecret({
name: `MyApp (${user.email})`,
issuer: 'MyApp'
});
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
// Store secret in database (encrypted!)
await User.update(user.id, {
mfaSecret: encrypt(secret.base32, encryptionKey),
mfaEnabled: false // Not enabled until verified
});
return {
secret: secret.base32,
qrCode: qrCodeUrl
};
}
// Verify MFA token
function verifyMFA(token, secret) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps before/after
});
}
// Login with MFA
async function loginWithMFA(email, password, mfaToken) {
// Verify password
const user = await User.findOne({email});
const passwordValid = await verifyPassword(password, user.passwordHash);
if (!passwordValid) {
throw new Error('Invalid credentials');
}
// If MFA enabled, verify token
if (user.mfaEnabled) {
const secret = decrypt(user.mfaSecret, encryptionKey);
const mfaValid = verifyMFA(mfaToken, secret);
if (!mfaValid) {
throw new Error('Invalid MFA token');
}
}
return generateJWT(user);
}
// === API KEY AUTHENTICATION ===
async function generateApiKey(userId) {
const apiKey = `sk_${generateSecureToken(32)}`;
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
await db.apiKeys.insert({
userId,
keyHash: hashedKey,
createdAt: new Date(),
lastUsed: null
});
return apiKey; // Show once, then forget
}
async function authenticateApiKey(apiKey) {
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const record = await db.apiKeys.findOne({keyHash: hashedKey});
if (!record) {
return null;
}
// Update last used
await db.apiKeys.update(record.id, {lastUsed: new Date()});
return await User.findById(record.userId);
}
// API key middleware
async function authenticateApi(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({error: 'API key required'});
}
const user = await authenticateApiKey(apiKey);
if (!user) {
return res.status(401).json({error: 'Invalid API key'});
}
req.user = user;
next();
}
Key Points: JWT for stateless auth - include expiration and
issuer claims. Use refresh tokens for long-lived sessions. RBAC for simple
permission models. ABAC for complex rules. OAuth 2.0 for third-party login. MFA for high-security accounts. Hash
API keys before storage. Validate tokens on every request. Use short-lived access tokens (15 min) with refresh
tokens (7 days). Store refresh tokens securely, support revocation. Always use HTTPS for auth.
25.6 Security Headers and HTTPS Integration
Essential Security Headers
| Header | Purpose | Recommended Value |
|---|---|---|
| Strict-Transport-Security | Force HTTPS | max-age=31536000; includeSubDomains |
| Content-Security-Policy | Prevent XSS, injection | default-src 'self'; script-src 'self' |
| X-Frame-Options | Prevent clickjacking | DENY or SAMEORIGIN |
| X-Content-Type-Options | Prevent MIME sniffing | nosniff |
| Referrer-Policy | Control referrer info | strict-origin-when-cross-origin |
| Permissions-Policy | Control browser features | geolocation=(), camera=() |
| X-XSS-Protection | XSS filter (legacy) | 1; mode=block |
CORS Headers
| Header | Purpose | Example Value |
|---|---|---|
| Access-Control-Allow-Origin | Allowed origins | https://example.com |
| Access-Control-Allow-Methods | Allowed HTTP methods | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Allowed request headers | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Allow cookies | true |
| Access-Control-Max-Age | Preflight cache time | 86400 (24 hours) |
HTTPS Best Practices
| Practice | Description | Benefit |
|---|---|---|
| TLS 1.3 | Use latest TLS version | Faster, more secure |
| Strong Ciphers | Disable weak ciphers | Prevent downgrade attacks |
| HSTS | Force HTTPS always | Prevent protocol downgrade |
| Certificate Pinning | Pin expected certificates | Prevent MITM with fake certs |
| OCSP Stapling | Include cert status | Faster cert validation |
Example: Security headers and HTTPS
// === SECURITY HEADERS MIDDLEWARE ===
// Using helmet (recommended)
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny'
},
noSniff: true,
xssFilter: true,
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
permissionsPolicy: {
features: {
geolocation: ["'none'"],
microphone: ["'none'"],
camera: ["'none'"],
payment: ["'self'"]
}
}
}));
// Manual security headers
function securityHeaders2(req, res, next) {
// HTTPS enforcement
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// XSS Protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Content Security Policy
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'"
);
// Permissions Policy
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=()'
);
next();
}
// === CORS CONFIGURATION ===
const cors = require('cors');
// Simple CORS
app.use(cors({
origin: 'https://example.com',
credentials: true
}));
// Advanced CORS with whitelist
const allowedOrigins = [
'https://example.com',
'https://app.example.com',
'http://localhost:3000'
];
app.use(cors({
origin: function(origin, callback) {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
maxAge: 86400 // 24 hours
}));
// Manual CORS middleware
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
}
// === HTTPS ENFORCEMENT ===
// Redirect HTTP to HTTPS
function forceHttps(req, res, next) {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
return next();
}
res.redirect(301, `https://${req.hostname}${req.url}`);
}
app.use(forceHttps);
// Create HTTPS server
const https = require('https');
const fs = require('fs');
const httpsOptions = {
key: fs.readFileSync('./certs/private-key.pem'),
cert: fs.readFileSync('./certs/certificate.pem'),
ca: fs.readFileSync('./certs/ca-bundle.pem'),
// TLS configuration
minVersion: 'TLSv1.3',
ciphers: [
'TLS_AES_128_GCM_SHA256',
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256'
].join(':'),
// Prefer server cipher order
honorCipherOrder: true,
// Enable OCSP stapling
// requestOCSP: true
};
https.createServer(httpsOptions, app).listen(443, () => {
console.log('HTTPS server running on port 443');
});
// === SUBRESOURCE INTEGRITY (SRI) ===
// Generate SRI hash
function generateSriHash(content) {
const hash = crypto.createHash('sha384').update(content).digest('base64');
return `sha384-${hash}`;
}
// Usage in HTML
// <script
// src="https://cdn.example.com/library.js"
// integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
// crossorigin="anonymous"
// ></script>
// === CERTIFICATE PINNING (Mobile/Native Apps) ===
// HTTP Public Key Pinning header (deprecated, use cert pinning in app)
// res.setHeader(
// 'Public-Key-Pins',
// 'pin-sha256="base64=="; max-age=5184000; includeSubDomains'
// );
// Certificate pinning in Node.js
const tls = require('tls');
const pinnedFingerprint = 'AA:BB:CC:DD:EE:FF:...';
function checkCertificate(socket) {
const cert = socket.getPeerCertificate();
const fingerprint = cert.fingerprint256;
if (fingerprint !== pinnedFingerprint) {
throw new Error('Certificate pinning failed');
}
}
// === RATE LIMITING WITH HEADERS ===
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
retryAfter: req.rateLimit.resetTime
});
}
});
app.use('/api/', limiter);
// Custom rate limit headers
function rateLimitHeaders(req, res, next) {
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', '95');
res.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + 900);
next();
}
// === SECURE COOKIES ===
// Set secure cookie
function setSecureCookie(res, name, value) {
res.cookie(name, value, {
httpOnly: true, // No JavaScript access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000, // 1 hour
domain: '.example.com',
path: '/',
signed: true // Sign cookie with secret
});
}
// === SECURITY.TXT FILE ===
// Serve security.txt for responsible disclosure
app.get('/.well-known/security.txt', (req, res) => {
res.type('text/plain');
res.send(`
Contact: security@example.com
Expires: 2026-12-31T23:59:59.000Z
Preferred-Languages: en
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/security-policy
Acknowledgments: https://example.com/hall-of-fame
`.trim());
});
// === SECURITY MONITORING ===
// Log security events
function logSecurityEvent(type, details) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
type,
details,
level: 'security'
}));
}
// Monitor suspicious activity
function detectSuspiciousActivity(req) {
// Check for SQL injection patterns
const sqlInjectionPattern = /(\bunion\b|\bselect\b|\bdrop\b|\binsert\b)/i;
if (sqlInjectionPattern.test(req.url) ||
sqlInjectionPattern.test(JSON.stringify(req.body))) {
logSecurityEvent('SQL_INJECTION_ATTEMPT', {
ip: req.ip,
url: req.url,
body: req.body
});
}
// Check for XSS patterns
const xssPattern = /(<script|javascript:|onerror=|onload=)/i;
if (xssPattern.test(JSON.stringify(req.body))) {
logSecurityEvent('XSS_ATTEMPT', {
ip: req.ip,
url: req.url
});
}
// Check for path traversal
if (req.url.includes('../') || req.url.includes('..\\')) {
logSecurityEvent('PATH_TRAVERSAL_ATTEMPT', {
ip: req.ip,
url: req.url
});
}
}
app.use((req, res, next) => {
detectSuspiciousActivity(req);
next();
});
Key Points: Use helmet.js for comprehensive security headers.
HSTS forces HTTPS. CSP prevents XSS and injection. X-Frame-Options prevents clickjacking. Configure CORS
properly
- whitelist origins. Use TLS 1.3 with strong ciphers. Set secure cookie flags (httpOnly, secure, sameSite).
Implement rate limiting. Use SRI for CDN resources. Certificate pinning for critical apps. Monitor security
events. Serve security.txt for disclosure. HTTPS everywhere - no exceptions.
Section 25 Summary: Security and Best Practices
- Input Validation: Validate all input server-side, whitelist over blacklist, prevent SQL/command/XSS injection
- Sanitization: Escape HTML entities, use parameterized queries, sanitize filenames, implement rate limiting
- XSS Prevention: Never use innerHTML with user input, avoid eval(), implement CSP headers, use DOMPurify
- CSP Directives: default-src, script-src (use nonces), style-src, img-src, connect-src, frame-ancestors
- Secure Coding: Never hardcode secrets, follow OWASP Top 10, defense in depth, least privilege principle
- Code Review: Check auth/authz, validate crypto usage, prevent sensitive data in errors/logs
- Password Hashing: Use bcrypt/Argon2 with salt, never plain MD5/SHA, use key derivation (PBKDF2)
- Encryption: AES-256-GCM for symmetric, RSA for asymmetric, random IV per encryption, HMAC for auth
- Authentication: JWT for stateless auth, refresh tokens for sessions, OAuth 2.0 for third-party, MFA for high security
- Authorization: RBAC for simple models, ABAC for complex rules, validate permissions on every request
- Security Headers: HSTS forces HTTPS, CSP prevents XSS, X-Frame-Options prevents clickjacking, use helmet.js
- HTTPS: Use TLS 1.3, strong ciphers, secure cookies (httpOnly, secure, sameSite), CORS whitelist