Cheatsheets

JavaScript

JavaScript

JavaScript is a versatile, high-level programming language that powers the web. It supports object-oriented, functional, and event-driven programming paradigms.

8 Categories 21 Sections 41 Examples
JavaScript ES6 Web Development Frontend Node.js Programming

Getting Started

Fundamental JavaScript concepts including variables, data types, and basic syntax.

Variables

Declaring and using variables with var, let, and const.

Variable declarations

let and const are block-scoped. var is function-scoped and hoisted. const prevents reassignment but does not make objects immutable.

Code
var oldWay = 'function scoped';
let mutable = 'can be reassigned';
const immutable = 'cannot be reassigned';
let x = 1;
x = 2; // OK
const y = 1;
// y = 2; // TypeError: Assignment to constant variable
Execution
Terminal window
node variables.js
  • Prefer const by default, use let only when reassignment is needed.
  • Avoid var in modern JavaScript.

Block scoping

var leaks out of blocks, while let and const are confined to the block where they are declared.

Code
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1
// console.log(b); // ReferenceError
// console.log(c); // ReferenceError
Execution
Terminal window
node scope.js
Output
1
  • Block scoping prevents variable leaks and accidental overwrites.
  • This is one of the main reasons to prefer let/const over var.

Data Types

JavaScript primitive and reference types.

Primitive types

JavaScript has 7 primitive types. typeof null returning "object" is a well-known historical bug.

Code
const str = 'hello'; // string
const num = 42; // number
const float = 3.14; // number
const bool = true; // boolean
const nothing = null; // object (historical bug)
const undef = undefined; // undefined
const sym = Symbol('id'); // symbol
const big = 9007199254740991n; // bigint
console.log(typeof str); // "string"
console.log(typeof num); // "number"
console.log(typeof nothing); // "object"
Execution
Terminal window
node types.js
Output
string
number
object
  • Use === instead of == to avoid type coercion surprises.
  • BigInt is for integers larger than Number.MAX_SAFE_INTEGER.

Type checking

typeof works for most types but returns "object" for null and arrays. Use Array.isArray() for arrays.

Code
typeof 'hello' // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" (bug)
typeof {} // "object"
typeof [] // "object"
Array.isArray([]) // true
typeof function(){} // "function"
  • Use instanceof for checking class instances.
  • null check: value === null.

Template Literals

String interpolation and multiline strings using backticks.

String interpolation

Template literals use backticks and ${} for embedding expressions directly in strings.

Code
const name = 'World';
const greeting = `Hello, ${name}!`;
console.log(greeting);
const a = 10, b = 20;
console.log(`Sum: ${a + b}`);
console.log(`Type: ${typeof name}`);
Execution
Terminal window
node template.js
Output
Hello, World!
Sum: 30
Type: string
  • Any valid JavaScript expression can go inside ${}.
  • Template literals preserve whitespace and newlines.

Multiline strings and tagged templates

Tagged templates allow custom processing of template literals through a function prefix.

Code
const multiline = `
This is a
multiline string
`;
function highlight(strings, ...values) {
return strings.reduce((result, str, i) =>
`${result}${str}<b>${values[i] || ''}</b>`, '');
}
const name = 'world';
console.log(highlight`Hello ${name}!`);
Execution
Terminal window
node tagged.js
Output
Hello <b>world</b>!<b></b>
  • Tagged templates are used in libraries like styled-components and GraphQL.
  • The tag function receives an array of string parts and interpolated values.

Operators

Comparison, logical, nullish coalescing, and optional chaining operators.

Comparison and logical operators

?? only checks null/undefined, unlike || which also catches 0, "", and false.

Code
// Equality
1 == '1' // true (type coercion)
1 === '1' // false (strict)
1 != '1' // false
1 !== '1' // true
// Logical
true && 'yes' // "yes"
false || 'fallback' // "fallback"
null ?? 'default' // "default" (nullish coalescing)
// Ternary
const age = 20;
const status = age >= 18 ? 'adult' : 'minor';
  • Always use === and !== to avoid type coercion bugs.
  • ?? is safer than || when 0 or empty string are valid values.

Optional chaining and nullish assignment

Optional chaining (?.) safely accesses nested properties without throwing if intermediate values are null/undefined.

Code
const user = {
name: 'Alice',
address: { city: 'NYC' }
};
// Optional chaining
console.log(user?.address?.city); // "NYC"
console.log(user?.phone?.number); // undefined
// Optional chaining with methods
console.log(user.toString?.()); // "[object Object]"
console.log(user.nonExistent?.()); // undefined
// Nullish assignment
let a = null;
a ??= 'default';
console.log(a); // "default"
let b = 0;
b ??= 42;
console.log(b); // 0 (not null/undefined)
Execution
Terminal window
node optional.js
Output
NYC
undefined
[object Object]
undefined
default
0
  • ?.() for optional method calls, ?.[] for optional bracket access.
  • Combine with ?? for safe default values.

Functions

Function declarations, expressions, arrow functions, and advanced patterns.

Arrow Functions

Concise function syntax introduced in ES6.

Arrow function syntax

Arrow functions provide concise syntax. Single expressions are implicitly returned. Multi-line bodies need explicit return.

Code
// Basic arrow function
const add = (a, b) => a + b;
// Single parameter (no parens needed)
const double = x => x * 2;
// No parameters
const greet = () => 'Hello!';
// Multi-line body (needs braces and return)
const sum = (a, b) => {
const result = a + b;
return result;
};
console.log(add(2, 3)); // 5
console.log(double(4)); // 8
console.log(greet()); // "Hello!"
Execution
Terminal window
node arrow.js
Output
5
8
Hello!
  • Arrow functions do not have their own this - they inherit it from the enclosing scope.
  • Cannot be used as constructors (no new keyword).

Returning objects

To return an object literal from an arrow function, wrap it in parentheses to avoid ambiguity with block syntax.

Code
// Wrap object literal in parentheses
const makeUser = (name, age) => ({ name, age });
console.log(makeUser('Alice', 30));
// { name: 'Alice', age: 30 }
// Common in array methods
const names = ['Alice', 'Bob'];
const users = names.map((name, i) => ({ id: i, name }));
console.log(users);
Execution
Terminal window
node arrow-obj.js
Output
{ name: 'Alice', age: 30 }
[ { id: 0, name: 'Alice' }, { id: 1, name: 'Bob' } ]
  • Without parentheses, {} is treated as a function body, not an object.
  • This pattern is very common with .map(), .filter(), and .reduce().

Default Parameters

Setting default values for function parameters.

Default parameter values

Default parameters are used when arguments are undefined or not provided. They can reference earlier parameters.

Code
function greet(name = 'World', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, World!"
console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Bob', 'Hi')); // "Hi, Bob!"
// Defaults can use previous parameters
function createUser(name, role = 'user', id = Date.now()) {
return { name, role, id };
}
Execution
Terminal window
node defaults.js
Output
Hello, World!
Hello, Alice!
Hi, Bob!
  • null does NOT trigger defaults, only undefined does.
  • Default values are evaluated at call time, not definition time.

Rest parameters

Rest parameters (...name) collect remaining arguments into a real array. Must be the last parameter.

Code
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20)); // 30
function tag(name, ...attrs) {
return `<${name} ${attrs.join(' ')}>`;
}
console.log(tag('div', 'class="box"', 'id="main"'));
Execution
Terminal window
node rest.js
Output
6
30
<div class="box" id="main">
  • Rest parameters replace the old arguments object.
  • Unlike arguments, rest parameters are a real Array.

Closures

Functions that capture variables from their enclosing scope.

Basic closure

The inner functions close over the count variable, maintaining access to it even after createCounter returns.

Code
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
Execution
Terminal window
node closure.js
Output
1
2
1
1
  • count is private and cannot be accessed directly from outside.
  • Each call to createCounter creates a new independent scope.

Factory function with closure

Each call to multiplier creates a new closure capturing its own factor value.

Code
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
console.log(double(10)); // 20
Execution
Terminal window
node factory.js
Output
10
15
20
  • Closures are the basis for many functional programming patterns.
  • They enable partial application and currying.

Objects and Arrays

Object manipulation, destructuring, spread operator, and array methods.

Destructuring

Extract values from objects and arrays into variables.

Object destructuring

Object destructuring extracts properties into variables. Supports renaming, defaults, and nesting.

Code
const user = { name: 'Alice', age: 30, city: 'NYC' };
// Basic destructuring
const { name, age } = user;
console.log(name, age); // "Alice" 30
// Rename variables
const { name: userName, age: userAge } = user;
console.log(userName); // "Alice"
// Default values
const { name: n, role = 'user' } = user;
console.log(role); // "user"
// Nested destructuring
const data = { a: { b: { c: 42 } } };
const { a: { b: { c } } } = data;
console.log(c); // 42
Execution
Terminal window
node destruct.js
Output
Alice 30
Alice
user
42
  • Destructuring works in function parameters too.
  • Use rest pattern { a, ...rest } to collect remaining properties.

Array destructuring

Array destructuring uses position to extract values. Supports skipping, rest patterns, and variable swapping.

Code
const colors = ['red', 'green', 'blue', 'yellow'];
// Basic
const [first, second] = colors;
console.log(first, second); // "red" "green"
// Skip elements
const [, , third] = colors;
console.log(third); // "blue"
// Rest pattern
const [head, ...tail] = colors;
console.log(tail); // ["green", "blue", "yellow"]
// Swap variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
Execution
Terminal window
node array-destruct.js
Output
red green
blue
["green", "blue", "yellow"]
2 1
  • Array destructuring works with any iterable (strings, maps, sets).
  • The swap pattern avoids needing a temporary variable.

Spread and Rest

The spread (...) operator for expanding and collecting elements.

Spread with arrays and objects

Spread expands iterables into individual elements. For objects, later properties override earlier ones.

Code
// Array spread
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]
// Array clone
const clone = [...arr1];
// Object spread
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: 'dark', lang: 'fr', fontSize: 14 }
Execution
Terminal window
node spread.js
Output
[1, 2, 3, 4, 5, 6]
{ theme: 'dark', lang: 'fr', fontSize: 14 }
  • Spread creates shallow copies, not deep clones.
  • Object spread is commonly used for immutable state updates.

Rest in function parameters and destructuring

Rest collects multiple elements into an array or object. Used in function parameters and destructuring.

Code
// Rest in functions
function log(first, ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
}
log('a', 'b', 'c');
// Rest in destructuring
const { a, ...others } = { a: 1, b: 2, c: 3 };
console.log(others); // { b: 2, c: 3 }
Execution
Terminal window
node rest-spread.js
Output
First: a
Rest: ["b", "c"]
{ b: 2, c: 3 }
  • Rest must be the last element in destructuring or parameter lists.
  • Rest in objects omits the explicitly destructured keys.

Array Methods

Essential array methods for transformation, filtering, and reduction.

Transform and filter

map transforms, filter selects, find returns first match, some/every test conditions, includes checks membership.

Code
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, ..., 20]
// filter - keep matching elements
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
// find - first match
const found = numbers.find(n => n > 3);
console.log(found); // 4
// some / every
console.log(numbers.some(n => n > 5)); // true
console.log(numbers.every(n => n > 0)); // true
// includes
console.log(numbers.includes(5)); // true
Execution
Terminal window
node array-methods.js
Output
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
[2, 4, 6, 8, 10]
4
true
true
true
  • These methods do not mutate the original array.
  • Chain methods for expressive data processing pipelines.

Reduce and flat

reduce accumulates array values. flat flattens nested arrays. flatMap maps and flattens in one step.

Code
const numbers = [1, 2, 3, 4, 5];
// reduce - accumulate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15
// reduce - group by
const people = [
{ name: 'Alice', dept: 'eng' },
{ name: 'Bob', dept: 'eng' },
{ name: 'Carol', dept: 'hr' }
];
const byDept = people.reduce((groups, person) => {
(groups[person.dept] ??= []).push(person);
return groups;
}, {});
console.log(byDept);
// flat and flatMap
const nested = [[1, 2], [3, [4, 5]]];
console.log(nested.flat()); // [1, 2, 3, [4, 5]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5]
const sentences = ['hello world', 'foo bar'];
const words = sentences.flatMap(s => s.split(' '));
console.log(words); // ["hello", "world", "foo", "bar"]
Execution
Terminal window
node reduce.js
Output
15
{ eng: [{...}, {...}], hr: [{...}] }
[1, 2, 3, [4, 5]]
[1, 2, 3, 4, 5]
["hello", "world", "foo", "bar"]
  • Always provide an initial value for reduce.
  • Use Object.groupBy() (ES2024) instead of reduce for grouping when available.

Object Methods

Key Object static methods for working with objects.

Object inspection methods

Object.keys/values/entries return arrays for iteration. Object.fromEntries converts entries back to an object.

Code
const user = { name: 'Alice', age: 30, city: 'NYC' };
console.log(Object.keys(user));
// ["name", "age", "city"]
console.log(Object.values(user));
// ["Alice", 30, "NYC"]
console.log(Object.entries(user));
// [["name","Alice"], ["age",30], ["city","NYC"]]
// Convert entries back to object
const filtered = Object.fromEntries(
Object.entries(user).filter(([k, v]) => typeof v === 'string')
);
console.log(filtered); // { name: 'Alice', city: 'NYC' }
Execution
Terminal window
node object-methods.js
Output
["name", "age", "city"]
["Alice", 30, "NYC"]
[["name","Alice"], ["age",30], ["city","NYC"]]
{ name: 'Alice', city: 'NYC' }
  • These methods only return own enumerable properties.
  • Object.entries + fromEntries is great for object transformation.

Computed properties and shorthand

Property shorthand, computed names, and method shorthand make object creation more concise.

Code
// Property shorthand
const name = 'Alice';
const age = 30;
const user = { name, age };
console.log(user); // { name: 'Alice', age: 30 }
// Computed property names
const key = 'color';
const obj = { [key]: 'blue', [`${key}Code`]: '#00f' };
console.log(obj); // { color: 'blue', colorCode: '#00f' }
// Method shorthand
const calc = {
value: 0,
add(n) { this.value += n; return this; },
subtract(n) { this.value -= n; return this; }
};
calc.add(5).subtract(2);
console.log(calc.value); // 3
Execution
Terminal window
node computed.js
Output
{ name: 'Alice', age: 30 }
{ color: 'blue', colorCode: '#00f' }
3
  • Computed properties can use any expression inside brackets.
  • Method shorthand has the same this behavior as regular functions.

Async JavaScript

Promises, async/await, and asynchronous patterns.

Promises

Creating and chaining promises for asynchronous operations.

Creating and using promises

Promises represent eventual completion or failure. Use .then() for success, .catch() for errors, .finally() for cleanup.

Code
// Creating a promise
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve({ id: 1, name: 'Alice' });
} else {
reject(new Error('Failed to fetch'));
}
}, 1000);
});
// Consuming with .then/.catch/.finally
fetchData
.then(data => console.log('Data:', data))
.catch(err => console.error('Error:', err))
.finally(() => console.log('Done'));
Execution
Terminal window
node promise.js
Output
Data: { id: 1, name: 'Alice' }
Done
  • Promises are always asynchronous, even if resolved immediately.
  • .catch() catches any error in the preceding chain.

Promise combinators

Promise.all fails on any rejection. allSettled waits for all. race returns the fastest. any returns first fulfillment.

Code
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject('error');
// all - waits for all (fails fast)
Promise.all([p1, p2])
.then(console.log); // [1, 2]
// allSettled - waits for all regardless
Promise.allSettled([p1, p3])
.then(console.log);
// [{status:'fulfilled',value:1}, {status:'rejected',reason:'error'}]
// race - first to settle
Promise.race([p1, p2])
.then(console.log); // 1
// any - first to fulfill
Promise.any([p3, p1])
.then(console.log); // 1
Execution
Terminal window
node combinators.js
Output
[1, 2]
[{status:'fulfilled',value:1},{status:'rejected',reason:'error'}]
1
1
  • Use Promise.all for parallel independent async operations.
  • Use Promise.allSettled when you need results regardless of individual failures.

Async/Await

Syntactic sugar over promises for cleaner asynchronous code.

Basic async/await

async functions always return promises. await pauses execution until the promise resolves. Use try/catch for error handling.

Code
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Not found');
const user = await response.json();
return user;
} catch (error) {
console.error('Failed:', error.message);
return null;
}
}
// Arrow async function
const getUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
// Top-level await (in modules)
const data = await fetchUser(1);
  • Top-level await works in ES modules only.
  • async/await is syntactic sugar over promises.

Sequential vs parallel execution

Use Promise.all with await for parallel execution of independent async operations. Sequential await is appropriate when operations depend on each other.

Code
// Sequential (slow - one after another)
async function sequential() {
const user = await fetchUser(1); // waits...
const posts = await fetchPosts(1); // then waits...
return { user, posts };
}
// Parallel (fast - both at once)
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1)
]);
return { user, posts };
}
// Parallel with error handling
async function parallelSafe() {
const results = await Promise.allSettled([
fetchUser(1),
fetchPosts(1)
]);
return results.map(r =>
r.status === 'fulfilled' ? r.value : null
);
}
  • Parallel execution can be significantly faster for independent operations.
  • Use for...of with await for sequential iteration over async operations.

Classes and Modules

ES6 classes, inheritance, and module import/export syntax.

Classes

ES6 class syntax for object-oriented programming.

Class declaration and inheritance

Classes support private fields (#), getters/setters, static methods, and inheritance via extends/super.

Code
class Animal {
#name; // private field
constructor(name) {
this.#name = name;
}
get name() { return this.#name; }
set name(value) { this.#name = value; }
speak() {
return `${this.#name} makes a sound`;
}
static create(name) {
return new Animal(name);
}
}
class Dog extends Animal {
#breed;
constructor(name, breed) {
super(name);
this.#breed = breed;
}
speak() {
return `${this.name} barks`;
}
info() {
return `${this.name} is a ${this.#breed}`;
}
}
const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // "Rex barks"
console.log(dog.info()); // "Rex is a Labrador"
const cat = Animal.create('Whiskers');
console.log(cat.speak()); // "Whiskers makes a sound"
Execution
Terminal window
node classes.js
Output
Rex barks
Rex is a Labrador
Whiskers makes a sound
  • Private fields (#) are truly private and not accessible outside the class.
  • Static methods are called on the class, not instances.

Class with static and instance methods

A practical class example implementing a simple event emitter with method chaining (returning this).

Code
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
return this;
}
emit(event, ...args) {
const handlers = this.#listeners.get(event) ?? [];
handlers.forEach(fn => fn(...args));
return this;
}
off(event, callback) {
const handlers = this.#listeners.get(event) ?? [];
this.#listeners.set(event,
handlers.filter(fn => fn !== callback)
);
return this;
}
}
const emitter = new EventEmitter();
emitter
.on('data', (msg) => console.log('Received:', msg))
.emit('data', 'hello');
Execution
Terminal window
node emitter.js
Output
Received: hello
  • Method chaining is enabled by returning this.
  • Private Map ensures listeners are encapsulated.

Modules

ES6 module import/export syntax.

Named and default exports

Named exports allow multiple exports per module. Default exports are imported without braces. Use as to rename imports.

Code
// math.js - Named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// utils.js - Default export
export default class Logger {
log(msg) { console.log(`[LOG] ${msg}`); }
}
// app.js - Importing
import Logger from './utils.js'; // default
import { add, multiply, PI } from './math.js'; // named
import { add as sum } from './math.js'; // rename
import * as math from './math.js'; // namespace
console.log(math.add(2, 3)); // 5
console.log(sum(2, 3)); // 5
console.log(PI); // 3.14159
  • A module can have one default export and many named exports.
  • Use namespace import (* as) to import all named exports.

Dynamic imports and re-exports

Dynamic imports return a promise and enable code splitting. Re-exports create barrel files for cleaner imports.

Code
// Dynamic import (code splitting)
async function loadModule() {
const { add } = await import('./math.js');
console.log(add(2, 3)); // 5
}
// Conditional import
const lang = 'en';
const messages = await import(`./i18n/${lang}.js`);
// Re-exports (index.js barrel file)
export { add, multiply } from './math.js';
export { default as Logger } from './utils.js';
export * from './helpers.js'; // re-export all
  • Dynamic imports are great for lazy loading in web apps.
  • Barrel files (index.js) simplify imports from complex modules.

Error Handling

Try/catch, custom errors, and error handling patterns.

Try/Catch

Exception handling with try, catch, and finally blocks.

Basic error handling

try/catch handles runtime errors. finally always executes. throw creates custom errors.

Code
try {
const data = JSON.parse('invalid json');
} catch (error) {
console.error('Parse error:', error.message);
} finally {
console.log('Always runs');
}
// Throwing custom errors
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
try {
console.log(divide(10, 0));
} catch (e) {
console.error(e.message); // "Division by zero"
}
Execution
Terminal window
node errors.js
Output
Parse error: Unexpected token i in JSON at position 0
Always runs
Division by zero
  • finally runs whether or not an error occurred.
  • Use specific error types for different error categories.

Custom error classes

Custom error classes extend Error for domain-specific error types. Use instanceof to handle specific error types.

Code
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
function validate(user) {
if (!user.name) throw new ValidationError('name', 'Name required');
if (!user.email) throw new ValidationError('email', 'Email required');
}
try {
validate({ name: '' });
} catch (e) {
if (e instanceof ValidationError) {
console.log(`${e.field}: ${e.message}`);
} else {
throw e; // re-throw unknown errors
}
}
Execution
Terminal window
node custom-error.js
Output
name: Name required
  • Always set this.name in custom errors for better debugging.
  • Re-throw errors you cannot handle at the current level.

Iterators and Generators

Iteration protocols, generators, and advanced iteration patterns.

Iterators

The iteration protocol and creating custom iterables.

Custom iterable

Objects implementing [Symbol.iterator]() are iterable and work with for...of, spread, and destructuring.

Code
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}
const range = new Range(1, 5);
for (const n of range) {
process.stdout.write(`${n} `);
}
// 1 2 3 4 5
console.log([...range]); // [1, 2, 3, 4, 5]
Execution
Terminal window
node iterator.js
Output
1 2 3 4 5
[1, 2, 3, 4, 5]
  • Built-in iterables include Array, String, Map, Set, and NodeList.
  • Spread operator and destructuring consume iterables.

Generators

Generator functions that can pause and resume execution.

Generator basics

Generator functions (function*) use yield to pause execution. Each next() call resumes until the next yield.

Code
function* count(start = 0) {
let i = start;
while (true) {
yield i++;
}
}
const counter = count(1);
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// Take first N from infinite generator
function* take(iterable, n) {
let i = 0;
for (const value of iterable) {
if (i++ >= n) return;
yield value;
}
}
console.log([...take(count(10), 5)]);
// [10, 11, 12, 13, 14]
Execution
Terminal window
node generator.js
Output
1
2
3
[10, 11, 12, 13, 14]
  • Generators are lazy; they only compute values on demand.
  • Infinite generators are safe because they only produce values when consumed.

Async generators

Async generators combine async/await with generator syntax. Use for await...of to consume them.

Code
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.length === 0) return;
yield data;
page++;
}
}
// Consuming async generator
async function getAllPages() {
const pages = [];
for await (const page of fetchPages('/api/items')) {
pages.push(...page);
if (pages.length > 100) break;
}
return pages;
}
  • Async generators are great for paginated API calls.
  • for await...of works with any async iterable.

Modern Features

Recent JavaScript features including Map, Set, Proxy, and other ES2020+ additions.

Map and Set

Map and Set collections for unique values and key-value pairs.

Map usage

Map allows any type as keys, maintains insertion order, and provides efficient key-value storage.

Code
const map = new Map();
// Any value can be a key
map.set('name', 'Alice');
map.set(42, 'the answer');
map.set(true, 'yes');
console.log(map.get('name')); // "Alice"
console.log(map.has(42)); // true
console.log(map.size); // 3
// Initialize from entries
const config = new Map([
['theme', 'dark'],
['lang', 'en'],
['debug', false]
]);
// Iteration
for (const [key, value] of config) {
console.log(`${key}: ${value}`);
}
// Convert to/from object
const obj = Object.fromEntries(config);
const map2 = new Map(Object.entries(obj));
Execution
Terminal window
node map.js
Output
Alice
true
3
theme: dark
lang: en
debug: false
  • Map is better than objects when keys are not strings.
  • Use map.size instead of Object.keys(obj).length.

Set usage

Set stores unique values of any type. Perfect for deduplication and membership testing.

Code
// Unique values
const set = new Set([1, 2, 3, 2, 1]);
console.log([...set]); // [1, 2, 3]
set.add(4);
set.delete(1);
console.log(set.has(2)); // true
console.log(set.size); // 3
// Array deduplication
const arr = [1, 1, 2, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]
// Set operations (ES2025+)
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
console.log([...a.intersection(b)]); // [2, 3]
console.log([...a.union(b)]); // [1, 2, 3, 4]
console.log([...a.difference(b)]); // [1]
Execution
Terminal window
node set.js
Output
[1, 2, 3]
true
3
[1, 2, 3, 4]
[2, 3]
[1, 2, 3, 4]
[1]
  • Set uses SameValueZero comparison (similar to ===).
  • Set operations (union, intersection, difference) are available in modern engines.

Proxy and Reflect

Metaprogramming with Proxy and Reflect for intercepting operations.

Validation proxy

Proxy intercepts operations (get, set, delete, etc.) on objects. Handlers define traps for each operation.

Code
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be 0-150');
}
}
target[prop] = value;
return true;
},
get(target, prop) {
if (prop in target) return target[prop];
throw new ReferenceError(`Property ${prop} not found`);
}
};
const user = new Proxy({}, validator);
user.name = 'Alice';
user.age = 30;
console.log(user.name); // "Alice"
try { user.age = -5; } catch (e) {
console.log(e.message); // "Age must be 0-150"
}
Execution
Terminal window
node proxy.js
Output
Alice
Age must be 0-150
  • Proxies are used in Vue 3 reactivity and MobX state management.
  • Use Reflect methods inside traps for default behavior.

Reactive proxy with onChange

A reactive proxy that calls onChange whenever properties are modified. This is the foundation of modern reactivity systems.

Code
function reactive(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return true;
},
deleteProperty(obj, prop) {
const value = obj[prop];
delete obj[prop];
onChange(prop, undefined, value);
return true;
}
});
}
const state = reactive({ count: 0 }, (prop, newVal, oldVal) => {
console.log(`${prop}: ${oldVal} -> ${newVal}`);
});
state.count = 1; // "count: 0 -> 1"
state.count = 5; // "count: 1 -> 5"
Execution
Terminal window
node reactive.js
Output
count: 0 -> 1
count: 1 -> 5
  • This pattern is the basis of Vue 3 and similar frameworks.
  • Add deep proxy wrapping for nested reactivity.

Miscellaneous Modern Features

Useful modern JavaScript features and syntax.

Useful modern additions

Modern JS includes structuredClone for deep copying, .at() for negative indexing, Object.groupBy for grouping, and more.

Code
// structuredClone - deep clone
const original = { a: { b: { c: 1 } }, d: [2, 3] };
const clone = structuredClone(original);
clone.a.b.c = 99;
console.log(original.a.b.c); // 1 (unchanged)
// Array.at() - negative indexing
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(-1)); // 5
console.log(arr.at(-2)); // 4
// Object.groupBy (ES2024)
const people = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Carol', age: 30 }
];
const byAge = Object.groupBy(people, p => p.age);
console.log(byAge[30]); // [Alice, Carol]
// String.replaceAll
const str = 'foo-bar-baz';
console.log(str.replaceAll('-', '_')); // "foo_bar_baz"
Execution
Terminal window
node modern.js
Output
1
5
4
[{name:'Alice',age:30},{name:'Carol',age:30}]
foo_bar_baz
  • structuredClone handles circular references but cannot clone functions.
  • Object.groupBy replaces manual reduce-based grouping.

Pattern matching with switch(true) and logical assignment

Logical assignment operators combine logical operations with assignment. switch(true) enables range-based matching.

Code
// Logical assignment operators
let a = null;
a ??= 'default'; // a = 'default' (null/undefined)
console.log(a);
let b = 0;
b ||= 42; // b = 42 (falsy)
console.log(b);
let c = 1;
c &&= 2; // c = 2 (truthy)
console.log(c);
// switch(true) pattern
const score = 85;
switch (true) {
case score >= 90: console.log('A'); break;
case score >= 80: console.log('B'); break;
case score >= 70: console.log('C'); break;
default: console.log('F');
}
Execution
Terminal window
node logical.js
Output
default
42
2
B
  • ??= assigns only if null/undefined.
  • ||= assigns if falsy (including 0, "", false).
  • &&= assigns only if truthy.