15 minlesson

Advanced JavaScript Patterns

Advanced JavaScript Patterns

Deep dive into advanced patterns using modern JavaScript features.

Deep Object Access Utility

Build a safe property accessor:

javascript
1function get(obj, path, defaultValue = undefined) {
2 const keys = path.split('.');
3 let result = obj;
4
5 for (const key of keys) {
6 result = result?.[key];
7 if (result === undefined) {
8 return defaultValue;
9 }
10 }
11
12 return result ?? defaultValue;
13}
14
15// Usage
16const data = {
17 user: {
18 profile: {
19 name: 'John',
20 settings: {
21 theme: 'dark'
22 }
23 }
24 }
25};
26
27get(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

javascript
1function getPath(obj, path, defaultValue = undefined) {
2 const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.');
3 let result = obj;
4
5 for (const key of keys) {
6 result = result?.[key];
7 if (result === undefined) {
8 return defaultValue;
9 }
10 }
11
12 return result ?? defaultValue;
13}
14
15// Now supports array indices
16const data = {
17 users: [
18 { name: 'Alice' },
19 { name: 'Bob' }
20 ]
21};
22
23getPath(data, 'users[0].name'); // 'Alice'
24getPath(data, 'users[1].name'); // 'Bob'
25getPath(data, 'users[99].name', 'Unknown'); // 'Unknown'

Memoization with Map

Cache function results:

javascript
1function memoize(fn) {
2 const cache = new Map();
3
4 return function(...args) {
5 const key = JSON.stringify(args);
6
7 if (cache.has(key)) {
8 return cache.get(key);
9 }
10
11 const result = fn.apply(this, args);
12 cache.set(key, result);
13 return result;
14 };
15}
16
17// Usage
18const expensiveCalc = memoize((n) => {
19 console.log('Computing...');
20 return n * n;
21});
22
23expensiveCalc(5); // Logs "Computing...", returns 25
24expensiveCalc(5); // Returns 25 (cached, no log)
25expensiveCalc(6); // Logs "Computing...", returns 36

Memoization with TTL

javascript
1function memoizeWithTTL(fn, ttl = 5000) {
2 const cache = new Map();
3
4 return function(...args) {
5 const key = JSON.stringify(args);
6 const cached = cache.get(key);
7
8 if (cached && Date.now() - cached.timestamp < ttl) {
9 return cached.value;
10 }
11
12 const result = fn.apply(this, args);
13 cache.set(key, { value: result, timestamp: Date.now() });
14 return result;
15 };
16}
17
18const 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:

javascript
1class LRUCache {
2 constructor(capacity) {
3 this.capacity = capacity;
4 this.cache = new Map();
5 }
6
7 get(key) {
8 if (!this.cache.has(key)) {
9 return undefined;
10 }
11
12 // Move to end (most recently used)
13 const value = this.cache.get(key);
14 this.cache.delete(key);
15 this.cache.set(key, value);
16
17 return value;
18 }
19
20 set(key, value) {
21 // If exists, delete first to update position
22 if (this.cache.has(key)) {
23 this.cache.delete(key);
24 }
25
26 this.cache.set(key, value);
27
28 // Remove oldest if over capacity
29 if (this.cache.size > this.capacity) {
30 const oldest = this.cache.keys().next().value;
31 this.cache.delete(oldest);
32 }
33 }
34
35 has(key) {
36 return this.cache.has(key);
37 }
38
39 clear() {
40 this.cache.clear();
41 }
42}
43
44// Usage
45const cache = new LRUCache(3);
46cache.set('a', 1);
47cache.set('b', 2);
48cache.set('c', 3);
49cache.get('a'); // Access 'a', moves it to recent
50cache.set('d', 4); // 'b' is evicted (least recent)

WeakMap for Private Data

Store private instance data:

javascript
1const privateData = new WeakMap();
2
3class Person {
4 constructor(name, ssn) {
5 this.name = name;
6 // SSN is private
7 privateData.set(this, { ssn });
8 }
9
10 getSSN(authToken) {
11 if (!this.verifyAuth(authToken)) {
12 throw new Error('Unauthorized');
13 }
14 return privateData.get(this).ssn;
15 }
16
17 verifyAuth(token) {
18 return token === 'secret';
19 }
20}
21
22const 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:

javascript
1const elementData = new WeakMap();
2
3function setElementData(element, data) {
4 const existing = elementData.get(element) || {};
5 elementData.set(element, { ...existing, ...data });
6}
7
8function getElementData(element) {
9 return elementData.get(element);
10}
11
12// Usage
13const button = document.querySelector('#myButton');
14setElementData(button, { clicks: 0, lastClick: null });
15
16button.addEventListener('click', () => {
17 const data = getElementData(button);
18 data.clicks++;
19 data.lastClick = Date.now();
20 setElementData(button, data);
21});
22
23// When button is removed from DOM and dereferenced,
24// its data is automatically garbage collected

Set for Deduplication

javascript
1// Simple deduplication
2const dedupe = (arr) => [...new Set(arr)];
3
4const ids = [1, 2, 2, 3, 3, 3, 4];
5dedupe(ids); // [1, 2, 3, 4]
6
7// Object deduplication by property
8function 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}
19
20const users = [
21 { id: 1, name: 'Alice' },
22 { id: 2, name: 'Bob' },
23 { id: 1, name: 'Alice (duplicate)' }
24];
25
26dedupeBy(users, u => u.id);
27// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

WeakSet for Object Tracking

Track processed objects without memory leaks:

javascript
1class ObjectTracker {
2 #processed = new WeakSet();
3
4 process(obj) {
5 if (this.#processed.has(obj)) {
6 console.log('Already processed');
7 return false;
8 }
9
10 this.#processed.add(obj);
11 // Process object...
12 return true;
13 }
14
15 isProcessed(obj) {
16 return this.#processed.has(obj);
17 }
18}
19
20const tracker = new ObjectTracker();
21const obj1 = { data: 'test' };
22
23tracker.process(obj1); // true (first time)
24tracker.process(obj1); // false (already processed)
25
26// When obj1 is no longer referenced, it's GC'd along with WeakSet entry

Generator Patterns

Pagination

javascript
1async function* paginate(fetchPage, pageSize = 10) {
2 let page = 0;
3 let hasMore = true;
4
5 while (hasMore) {
6 const data = await fetchPage(page, pageSize);
7
8 if (data.items.length === 0) {
9 hasMore = false;
10 } else {
11 yield* data.items;
12 page++;
13 hasMore = data.hasMore;
14 }
15 }
16}
17
18// Usage
19const pages = paginate(async (page, size) => {
20 const response = await fetch(`/api/items?page=${page}&size=${size}`);
21 return response.json();
22});
23
24for await (const item of pages) {
25 console.log(item);
26 // Automatically fetches next page when needed
27}

State Machine

javascript
1function* stateMachine() {
2 let state = 'idle';
3
4 while (true) {
5 const action = yield state;
6
7 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}
22
23const 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

javascript
1function createValidatedObject(schema) {
2 return new Proxy({}, {
3 set(target, prop, value) {
4 const validator = schema[prop];
5
6 if (validator && !validator(value)) {
7 throw new Error(`Invalid value for ${prop}`);
8 }
9
10 target[prop] = value;
11 return true;
12 }
13 });
14}
15
16const 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};
21
22const user = createValidatedObject(userSchema);
23user.name = 'John'; // OK
24user.age = 30; // OK
25user.email = 'invalid'; // Error: Invalid value for email

Observable Proxy

javascript
1function 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}
11
12const state = observable({ count: 0 }, (prop, newVal, oldVal) => {
13 console.log(`${prop} changed from ${oldVal} to ${newVal}`);
14 // Update UI, trigger effects, etc.
15});
16
17state.count++; // Logs: "count changed from 0 to 1"
18state.count++; // Logs: "count changed from 1 to 2"

Default Values Proxy

javascript
1function withDefaults(target, defaults) {
2 return new Proxy(target, {
3 get(obj, prop) {
4 return prop in obj ? obj[prop] : defaults[prop];
5 }
6 });
7}
8
9const config = withDefaults(
10 { theme: 'dark' },
11 { theme: 'light', fontSize: 14, language: 'en' }
12);
13
14config.theme; // 'dark' (from target)
15config.fontSize; // 14 (from defaults)
16config.language; // 'en' (from defaults)

Summary

PatternFeature UsedUse Case
Safe Access?., ??Nested property access
MemoizationMapFunction result caching
LRU CacheMapBounded caching
Private DataWeakMapHidden instance data
DOM MetadataWeakMapElement-attached data
DeduplicationSetUnique values
Object TrackingWeakSetProcess-once patterns
PaginationGeneratorsLazy data loading
State MachineGeneratorsState management
ValidationProxyRuntime type checking
ObservableProxyReactive state

Key takeaways:

  • Use Map/Set for complex collections
  • Use WeakMap/WeakSet to prevent memory leaks
  • Use generators for lazy evaluation
  • Use Proxy for metaprogramming