# Languages — JavaScript Patterns # JavaScript ## When to Use - Working with JavaScript files (.js, .mjs) - Browser scripting - Node.js applications without TypeScript ## When NOT to Use - TypeScript projects -- use the `typescript` skill instead, which covers typed JavaScript patterns - Python-only projects with no JavaScript components --- ## Core Patterns ### 1. Modern Syntax #### Destructuring (Nested, Defaults, Rest) ```javascript // Object destructuring with defaults and rename const { name, email, role = "user", address: { city } = {} } = user; // Nested destructuring const { data: { attributes: { title, body }, }, } = apiResponse; // Array destructuring with rest const [first, second, ...remaining] = items; // Swap variables let a = 1, b = 2; [a, b] = [b, a]; // Function parameter destructuring function createUser({ name, email, role = "user" }) { return { name, email, role, createdAt: new Date() }; } ``` #### Optional Chaining (?.) ```javascript // Property access const city = user?.address?.city; // Method call const uppercased = value?.toString?.(); // Array element const firstItem = data?.items?.[0]; // Combine with nullish coalescing for defaults const displayName = user?.profile?.displayName ?? user?.name ?? "Anonymous"; ``` #### Nullish Coalescing (??) ```javascript // Only falls through on null/undefined (not 0, "", false) const port = config.port ?? 3000; const name = input ?? "default"; // Contrast with || which falls through on all falsy values const count = data.count ?? 0; // preserves 0 const count2 = data.count || 0; // replaces 0 with 0 (same here, but misleading) const label = data.label ?? ""; // preserves "" const label2 = data.label || "fallback"; // replaces "" with "fallback" ``` #### Logical Assignment (&&=, ||=, ??=) ```javascript // ??= assigns only if null/undefined user.name ??= "Anonymous"; // ||= assigns if falsy config.retries ||= 3; // &&= assigns only if truthy user.session &&= refreshSession(user.session); // Practical: initialize nested objects const cache = {}; (cache.users ??= []).push(newUser); ``` --- ### 2. Async Patterns #### Promises ```javascript function fetchJson(url) { return fetch(url) .then((response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); } // Chaining fetchJson("/api/user") .then((user) => fetchJson(`/api/posts?userId=${user.id}`)) .then((posts) => console.log(posts)) .catch((error) => console.error("Failed:", error.message)); ``` #### async/await ```javascript async function loadUserDashboard(userId) { const user = await fetchJson(`/api/users/${userId}`); const posts = await fetchJson(`/api/users/${userId}/posts`); return { user, posts }; } ``` #### Promise.all / allSettled / race / any ```javascript // Promise.all -- fail fast on first rejection const [users, posts, comments] = await Promise.all([ fetchJson("/api/users"), fetchJson("/api/posts"), fetchJson("/api/comments"), ]); // Promise.allSettled -- wait for all, get status of each const results = await Promise.allSettled([ fetchJson("/api/fast"), fetchJson("/api/slow"), fetchJson("/api/flaky"), ]); const successes = results .filter((r) => r.status === "fulfilled") .map((r) => r.value); const failures = results .filter((r) => r.status === "rejected") .map((r) => r.reason); // Promise.race -- first to settle wins const result = await Promise.race([ fetchJson("/api/primary"), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000) ), ]); // Promise.any -- first to fulfill wins (ignores rejections) const fastest = await Promise.any([ fetchJson("/api/mirror1"), fetchJson("/api/mirror2"), fetchJson("/api/mirror3"), ]); ``` #### AbortController ```javascript async function fetchWithTimeout(url, timeoutMs = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); return await response.json(); } finally { clearTimeout(timeoutId); } } // Cancellable request pattern function createRequest(url) { const controller = new AbortController(); return { promise: fetch(url, { signal: controller.signal }), cancel: () => controller.abort(), }; } ``` #### Async Iterators (for await...of) ```javascript async function* paginateApi(baseUrl) { let page = 1; while (true) { const data = await fetchJson(`${baseUrl}?page=${page}`); if (data.items.length === 0) break; yield* data.items; page++; } } // Consume the async iterator for await (const item of paginateApi("/api/records")) { processItem(item); } ``` --- ### 3. Closures & Scope #### Closure Patterns ```javascript // Counter with private state function createCounter(initial = 0) { let count = initial; return { increment: () => ++count, decrement: () => --count, getCount: () => count, reset: () => { count = initial; }, }; } const counter = createCounter(10); counter.increment(); // 11 counter.getCount(); // 11 ``` #### Module Pattern (Private State via Closures) ```javascript const rateLimiter = (() => { const requests = new Map(); function isAllowed(clientId, maxPerMinute = 60) { const now = Date.now(); const windowStart = now - 60_000; const clientRequests = (requests.get(clientId) ?? []).filter( (t) => t > windowStart ); if (clientRequests.length >= maxPerMinute) return false; clientRequests.push(now); requests.set(clientId, clientRequests); return true; } function reset(clientId) { requests.delete(clientId); } return { isAllowed, reset }; })(); ``` #### WeakRef and FinalizationRegistry ```javascript // Cache that does not prevent garbage collection const cache = new Map(); function getCached(key, factory) { const ref = cache.get(key); const cached = ref?.deref(); if (cached !== undefined) return cached; const value = factory(); cache.set(key, new WeakRef(value)); return value; } // Cleanup when objects are garbage collected const registry = new FinalizationRegistry((key) => { cache.delete(key); }); ``` --- ### 4. Iteration Protocols #### Custom Iterator ```javascript class Range { constructor(start, end, step = 1) { this.start = start; this.end = end; this.step = step; } [Symbol.iterator]() { let current = this.start; const { end, step } = this; return { next() { if (current < end) { const value = current; current += step; return { value, done: false }; } return { done: true }; }, }; } } for (const n of new Range(0, 10, 2)) { console.log(n); // 0, 2, 4, 6, 8 } ``` #### Generators ```javascript function* fibonacci() { let a = 0, b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } // Take first N values function take(iterable, count) { const result = []; for (const value of iterable) { result.push(value); if (result.length >= count) break; } return result; } take(fibonacci(), 8); // [0, 1, 1, 2, 3, 5, 8, 13] ``` #### Lazy Evaluation with Generators ```javascript function* map(iterable, fn) { for (const item of iterable) { yield fn(item); } } function* filter(iterable, predicate) { for (const item of iterable) { if (predicate(item)) yield item; } } // Compose lazily -- no intermediate arrays const data = filter( map(readLargeFile(), (line) => line.trim()), (line) => line.length > 0 ); ``` #### Async Generators ```javascript async function* readChunks(reader) { while (true) { const { done, value } = await reader.read(); if (done) break; yield value; } } // Stream processing const response = await fetch("/api/large-data"); for await (const chunk of readChunks(response.body.getReader())) { processChunk(chunk); } ``` --- ### 5. Proxy & Reflect #### Validation Proxy ```javascript function createValidated(target, validators) { return new Proxy(target, { set(obj, prop, value) { const validate = validators[prop]; if (validate && !validate(value)) { throw new TypeError(`Invalid value for ${String(prop)}: ${value}`); } return Reflect.set(obj, prop, value); }, }); } const user = createValidated( { name: "", age: 0 }, { name: (v) => typeof v === "string" && v.length > 0, age: (v) => typeof v === "number" && v >= 0 && v <= 150, } ); user.name = "Alice"; // works user.age = -1; // throws TypeError ``` #### Observable Object ```javascript function createObservable(target, onChange) { return new Proxy(target, { set(obj, prop, value) { const oldValue = obj[prop]; const result = Reflect.set(obj, prop, value); if (oldValue !== value) { onChange(prop, value, oldValue); } return result; }, deleteProperty(obj, prop) { const oldValue = obj[prop]; const result = Reflect.deleteProperty(obj, prop); onChange(prop, undefined, oldValue); return result; }, }); } const state = createObservable({}, (prop, newVal, oldVal) => { console.log(`${prop}: ${oldVal} -> ${newVal}`); }); ``` #### Property Access Logging ```javascript function withLogging(target, label = "access") { return new Proxy(target, { get(obj, prop) { console.log(`[${label}] get .${String(prop)}`); return Reflect.get(obj, prop); }, has(obj, prop) { console.log(`[${label}] has .${String(prop)}`); return Reflect.has(obj, prop); }, }); } ``` --- ### 6. Module System #### ESM (import/export) ```javascript // Named exports export function formatDate(date) { ... } export const MAX_RETRIES = 3; // Default export export default class ApiClient { ... } // Re-exports export { formatDate } from "./utils.js"; export { default as ApiClient } from "./api-client.js"; ``` #### Dynamic import() ```javascript // Lazy load modules async function loadChart(type) { const module = await import(`./charts/${type}.js`); return new module.default(); } // Conditional loading const { marked } = await import("marked"); // With error handling async function tryLoadPlugin(name) { try { return await import(`./plugins/${name}.js`); } catch { console.warn(`Plugin ${name} not available`); return null; } } ``` #### import.meta ```javascript // Current module URL console.log(import.meta.url); // Resolve relative paths (Node.js) const configPath = new URL("./config.json", import.meta.url); // Check if file is the entry point (Node.js) if (import.meta.url === `file://${process.argv[1]}`) { main(); } // Vite environment variables const apiUrl = import.meta.env.VITE_API_URL; ``` #### Top-level await ```javascript // config.js -- top-level await in ESM modules const response = await fetch("/api/config"); export const config = await response.json(); // db.js import { createPool } from "./db-pool.js"; export const pool = await createPool(process.env.DATABASE_URL); ``` --- ### 7. Performance #### structuredClone ```javascript // Deep clone without library (replaces JSON.parse(JSON.stringify(...))) const original = { nested: { array: [1, 2, 3], date: new Date() } }; const clone = structuredClone(original); // Handles Date, Map, Set, ArrayBuffer, RegExp (but not functions) clone.nested.array.push(4); console.log(original.nested.array.length); // still 3 ``` #### requestAnimationFrame ```javascript // Smooth animation loop function animate(timestamp) { updatePosition(timestamp); render(); requestAnimationFrame(animate); } requestAnimationFrame(animate); // Throttle DOM updates to frame rate let rafId = null; function scheduleUpdate(data) { if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { applyDOMUpdate(data); rafId = null; }); } ``` #### requestIdleCallback ```javascript // Run low-priority work when the browser is idle function processQueue(queue) { requestIdleCallback((deadline) => { while (deadline.timeRemaining() > 0 && queue.length > 0) { const task = queue.shift(); task(); } if (queue.length > 0) { processQueue(queue); // schedule remaining } }); } ``` #### Web Workers Basics ```javascript // main.js const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module", }); worker.postMessage({ data: largeDataSet, operation: "sort" }); worker.onmessage = (event) => { const sorted = event.data; renderResults(sorted); }; // worker.js self.onmessage = (event) => { const { data, operation } = event.data; if (operation === "sort") { self.postMessage(data.sort((a, b) => a - b)); } }; ``` #### performance.mark / measure ```javascript // Measure operation duration performance.mark("fetch-start"); const data = await fetchJson("/api/data"); performance.mark("fetch-end"); performance.measure("fetch-duration", "fetch-start", "fetch-end"); const measurement = performance.getEntriesByName("fetch-duration")[0]; console.log(`Fetch took ${measurement.duration.toFixed(2)}ms`); // Clean up performance.clearMarks(); performance.clearMeasures(); ``` --- ## Best Practices 1. **Use `const` by default, `let` only when reassignment is needed** -- never use `var`. Block scoping prevents entire categories of bugs from hoisting and accidental mutation. 2. **Handle all promise rejections** -- unhandled rejections crash Node.js processes. Always use try/catch with await, or attach `.catch()` to promise chains. Add a global handler as a safety net. ```javascript process.on("unhandledRejection", (reason) => { console.error("Unhandled rejection:", reason); process.exit(1); }); ``` 3. **Use arrow functions for callbacks, regular functions for methods** -- arrow functions capture `this` from the enclosing scope, which is correct for callbacks but breaks object methods that need their own `this`. 4. **Prefer `for...of` over `for...in` for iteration** -- `for...in` iterates over all enumerable properties including inherited ones. Use `for...of` for arrays and iterables, `Object.entries()` for objects. 5. **Use ESLint and Prettier** -- enforce consistent style automatically. Configure in the project root and run on pre-commit hooks. 6. **Avoid mutating function arguments** -- create new objects and arrays with spread syntax instead of modifying inputs in place. This prevents action-at-a-distance bugs. 7. **Use `structuredClone` for deep copies** -- replaces the `JSON.parse(JSON.stringify(x))` hack. Handles Dates, Maps, Sets, and circular references correctly. 8. **Use private class fields (`#field`)** -- the `#` prefix creates truly private fields that cannot be accessed outside the class, unlike the `_` convention which is only a hint. --- ## Common Pitfalls 1. **Implicit type coercion** -- always use `===` and `!==`. The `==` operator performs type coercion with surprising rules (`"" == false`, `0 == null` is false but `0 == undefined` is also false, yet `null == undefined` is true). 2. **Forgetting `await`** -- a missing `await` silently returns a Promise object instead of the resolved value, causing hard-to-debug issues. ```javascript // BAD -- data is a Promise, not the response const data = fetchJson("/api/data"); // GOOD const data = await fetchJson("/api/data"); ``` 3. **`this` binding in callbacks** -- regular functions in callbacks lose their `this` context. Use arrow functions or `.bind()`. ```javascript // BAD class Timer { start() { setTimeout(function() { this.tick(); }, 1000); } } // GOOD class Timer { start() { setTimeout(() => this.tick(), 1000); } } ``` 4. **Mutating objects passed by reference** -- objects and arrays are passed by reference. Modifying a parameter modifies the original. ```javascript // BAD function addDefaults(config) { config.retries = config.retries ?? 3; // mutates caller's object return config; } // GOOD function addDefaults(config) { return { retries: 3, ...config }; } ``` 5. **`for...in` on arrays** -- iterates over indices as strings and includes inherited properties. Use `for...of` or array methods. ```javascript // BAD for (const i in [10, 20, 30]) { console.log(typeof i); // "string", not "number" } // GOOD for (const value of [10, 20, 30]) { console.log(value); // 10, 20, 30 } ``` 6. **Floating point arithmetic** -- `0.1 + 0.2 !== 0.3` in JavaScript. For financial calculations, work in integer cents or use a decimal library. ```javascript // BAD const total = 0.1 + 0.2; // 0.30000000000000004 // GOOD const totalCents = 10 + 20; // 30 const total = totalCents / 100; // 0.3 ``` --- ## Related Skills - `typescript` -- TypeScript for typed JavaScript development - `react` -- React component patterns - `nextjs` -- Next.js full-stack framework - `vitest` -- JavaScript/TypeScript testing with Vitest