Advanced JavaScript Patterns
Deep dive into advanced patterns using modern JavaScript features.
Deep Object Access Utility
Build a safe property accessor:
javascript1function get(obj, path, defaultValue = undefined) {2 const keys = path.split('.');3 let result = obj;45 for (const key of keys) {6 result = result?.[key];7 if (result === undefined) {8 return defaultValue;9 }10 }1112 return result ?? defaultValue;13}1415// Usage16const data = {17 user: {18 profile: {19 name: 'John',20 settings: {21 theme: 'dark'22 }23 }24 }25};2627get(data, 'user.profile.name'); // 'John'28get(data, 'user.profile.settings.theme'); // 'dark'29get(data, 'user.profile.missing', 'default'); // 'default'30get(data, 'invalid.path'); // undefined
Array Access in Path
javascript1function getPath(obj, path, defaultValue = undefined) {2 const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.');3 let result = obj;45 for (const key of keys) {6 result = result?.[key];7 if (result === undefined) {8 return defaultValue;9 }10 }1112 return result ?? defaultValue;13}1415// Now supports array indices16const data = {17 users: [18 { name: 'Alice' },19 { name: 'Bob' }20 ]21};2223getPath(data, 'users[0].name'); // 'Alice'24getPath(data, 'users[1].name'); // 'Bob'25getPath(data, 'users[99].name', 'Unknown'); // 'Unknown'
Memoization with Map
Cache function results:
javascript1function memoize(fn) {2 const cache = new Map();34 return function(...args) {5 const key = JSON.stringify(args);67 if (cache.has(key)) {8 return cache.get(key);9 }1011 const result = fn.apply(this, args);12 cache.set(key, result);13 return result;14 };15}1617// Usage18const expensiveCalc = memoize((n) => {19 console.log('Computing...');20 return n * n;21});2223expensiveCalc(5); // Logs "Computing...", returns 2524expensiveCalc(5); // Returns 25 (cached, no log)25expensiveCalc(6); // Logs "Computing...", returns 36
Memoization with TTL
javascript1function memoizeWithTTL(fn, ttl = 5000) {2 const cache = new Map();34 return function(...args) {5 const key = JSON.stringify(args);6 const cached = cache.get(key);78 if (cached && Date.now() - cached.timestamp < ttl) {9 return cached.value;10 }1112 const result = fn.apply(this, args);13 cache.set(key, { value: result, timestamp: Date.now() });14 return result;15 };16}1718const fetchUser = memoizeWithTTL(async (id) => {19 const response = await fetch(`/api/users/${id}`);20 return response.json();21}, 30000); // Cache for 30 seconds
LRU Cache with Map
Least Recently Used cache:
javascript1class LRUCache {2 constructor(capacity) {3 this.capacity = capacity;4 this.cache = new Map();5 }67 get(key) {8 if (!this.cache.has(key)) {9 return undefined;10 }1112 // Move to end (most recently used)13 const value = this.cache.get(key);14 this.cache.delete(key);15 this.cache.set(key, value);1617 return value;18 }1920 set(key, value) {21 // If exists, delete first to update position22 if (this.cache.has(key)) {23 this.cache.delete(key);24 }2526 this.cache.set(key, value);2728 // Remove oldest if over capacity29 if (this.cache.size > this.capacity) {30 const oldest = this.cache.keys().next().value;31 this.cache.delete(oldest);32 }33 }3435 has(key) {36 return this.cache.has(key);37 }3839 clear() {40 this.cache.clear();41 }42}4344// Usage45const cache = new LRUCache(3);46cache.set('a', 1);47cache.set('b', 2);48cache.set('c', 3);49cache.get('a'); // Access 'a', moves it to recent50cache.set('d', 4); // 'b' is evicted (least recent)
WeakMap for Private Data
Store private instance data:
javascript1const privateData = new WeakMap();23class Person {4 constructor(name, ssn) {5 this.name = name;6 // SSN is private7 privateData.set(this, { ssn });8 }910 getSSN(authToken) {11 if (!this.verifyAuth(authToken)) {12 throw new Error('Unauthorized');13 }14 return privateData.get(this).ssn;15 }1617 verifyAuth(token) {18 return token === 'secret';19 }20}2122const person = new Person('John', '123-45-6789');23console.log(person.name); // 'John'24console.log(person.ssn); // undefined (not accessible)25console.log(person.getSSN('secret')); // '123-45-6789'
DOM Metadata with WeakMap
Attach data to DOM elements without memory leaks:
javascript1const elementData = new WeakMap();23function setElementData(element, data) {4 const existing = elementData.get(element) || {};5 elementData.set(element, { ...existing, ...data });6}78function getElementData(element) {9 return elementData.get(element);10}1112// Usage13const button = document.querySelector('#myButton');14setElementData(button, { clicks: 0, lastClick: null });1516button.addEventListener('click', () => {17 const data = getElementData(button);18 data.clicks++;19 data.lastClick = Date.now();20 setElementData(button, data);21});2223// When button is removed from DOM and dereferenced,24// its data is automatically garbage collected
Set for Deduplication
javascript1// Simple deduplication2const dedupe = (arr) => [...new Set(arr)];34const ids = [1, 2, 2, 3, 3, 3, 4];5dedupe(ids); // [1, 2, 3, 4]67// Object deduplication by property8function dedupeBy(arr, keyFn) {9 const seen = new Set();10 return arr.filter(item => {11 const key = keyFn(item);12 if (seen.has(key)) {13 return false;14 }15 seen.add(key);16 return true;17 });18}1920const users = [21 { id: 1, name: 'Alice' },22 { id: 2, name: 'Bob' },23 { id: 1, name: 'Alice (duplicate)' }24];2526dedupeBy(users, u => u.id);27// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
WeakSet for Object Tracking
Track processed objects without memory leaks:
javascript1class ObjectTracker {2 #processed = new WeakSet();34 process(obj) {5 if (this.#processed.has(obj)) {6 console.log('Already processed');7 return false;8 }910 this.#processed.add(obj);11 // Process object...12 return true;13 }1415 isProcessed(obj) {16 return this.#processed.has(obj);17 }18}1920const tracker = new ObjectTracker();21const obj1 = { data: 'test' };2223tracker.process(obj1); // true (first time)24tracker.process(obj1); // false (already processed)2526// When obj1 is no longer referenced, it's GC'd along with WeakSet entry
Generator Patterns
Pagination
javascript1async function* paginate(fetchPage, pageSize = 10) {2 let page = 0;3 let hasMore = true;45 while (hasMore) {6 const data = await fetchPage(page, pageSize);78 if (data.items.length === 0) {9 hasMore = false;10 } else {11 yield* data.items;12 page++;13 hasMore = data.hasMore;14 }15 }16}1718// Usage19const pages = paginate(async (page, size) => {20 const response = await fetch(`/api/items?page=${page}&size=${size}`);21 return response.json();22});2324for await (const item of pages) {25 console.log(item);26 // Automatically fetches next page when needed27}
State Machine
javascript1function* stateMachine() {2 let state = 'idle';34 while (true) {5 const action = yield state;67 switch (state) {8 case 'idle':9 if (action === 'start') state = 'running';10 break;11 case 'running':12 if (action === 'pause') state = 'paused';13 if (action === 'stop') state = 'idle';14 break;15 case 'paused':16 if (action === 'resume') state = 'running';17 if (action === 'stop') state = 'idle';18 break;19 }20 }21}2223const machine = stateMachine();24machine.next(); // { value: 'idle' }25machine.next('start'); // { value: 'running' }26machine.next('pause'); // { value: 'paused' }27machine.next('resume'); // { value: 'running' }28machine.next('stop'); // { value: 'idle' }
Proxy Patterns
Validation Proxy
javascript1function createValidatedObject(schema) {2 return new Proxy({}, {3 set(target, prop, value) {4 const validator = schema[prop];56 if (validator && !validator(value)) {7 throw new Error(`Invalid value for ${prop}`);8 }910 target[prop] = value;11 return true;12 }13 });14}1516const userSchema = {17 name: (v) => typeof v === 'string' && v.length > 0,18 age: (v) => typeof v === 'number' && v >= 0 && v <= 150,19 email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)20};2122const user = createValidatedObject(userSchema);23user.name = 'John'; // OK24user.age = 30; // OK25user.email = 'invalid'; // Error: Invalid value for email
Observable Proxy
javascript1function observable(target, callback) {2 return new Proxy(target, {3 set(obj, prop, value) {4 const oldValue = obj[prop];5 obj[prop] = value;6 callback(prop, value, oldValue);7 return true;8 }9 });10}1112const state = observable({ count: 0 }, (prop, newVal, oldVal) => {13 console.log(`${prop} changed from ${oldVal} to ${newVal}`);14 // Update UI, trigger effects, etc.15});1617state.count++; // Logs: "count changed from 0 to 1"18state.count++; // Logs: "count changed from 1 to 2"
Default Values Proxy
javascript1function withDefaults(target, defaults) {2 return new Proxy(target, {3 get(obj, prop) {4 return prop in obj ? obj[prop] : defaults[prop];5 }6 });7}89const config = withDefaults(10 { theme: 'dark' },11 { theme: 'light', fontSize: 14, language: 'en' }12);1314config.theme; // 'dark' (from target)15config.fontSize; // 14 (from defaults)16config.language; // 'en' (from defaults)
Summary
| Pattern | Feature Used | Use Case |
|---|---|---|
| Safe Access | ?., ?? | Nested property access |
| Memoization | Map | Function result caching |
| LRU Cache | Map | Bounded caching |
| Private Data | WeakMap | Hidden instance data |
| DOM Metadata | WeakMap | Element-attached data |
| Deduplication | Set | Unique values |
| Object Tracking | WeakSet | Process-once patterns |
| Pagination | Generators | Lazy data loading |
| State Machine | Generators | State management |
| Validation | Proxy | Runtime type checking |
| Observable | Proxy | Reactive state |
Key takeaways:
- Use
Map/Setfor complex collections - Use
WeakMap/WeakSetto prevent memory leaks - Use generators for lazy evaluation
- Use Proxy for metaprogramming