Steven's Knowledge
Advanced Features

Advanced Patterns

WeakRef, AbortController, Symbols, Iterators, Generators, and Broadcast Channel — advanced JavaScript patterns for memory, control flow, and communication

Advanced Patterns

Advanced JavaScript features that solve specific, often overlooked problems — memory management, cancellation, lazy evaluation, cross-context communication, and meta-programming primitives.


WeakRef & FinalizationRegistry

Problem

Standard references prevent garbage collection. Caches and object mappings can cause memory leaks when they hold strong references to objects that are no longer needed.

WeakRef

Creates a reference that does not prevent garbage collection:

class Cache {
  #entries = new Map();

  set(key, value) {
    this.#entries.set(key, new WeakRef(value));
  }

  get(key) {
    const ref = this.#entries.get(key);
    if (!ref) return undefined;

    const value = ref.deref(); // Returns undefined if GC'd
    if (!value) {
      this.#entries.delete(key); // Clean up stale entry
      return undefined;
    }
    return value;
  }
}

const cache = new Cache();
let hugeData = { /* large object */ };
cache.set('report', hugeData);

hugeData = null; // Object can now be garbage collected
// cache.get('report') may return undefined after GC runs

FinalizationRegistry

Runs a callback after an object is garbage collected:

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with ID ${heldValue} was garbage collected`);
  // Cleanup: close file handles, release native resources, update maps
});

function trackObject(id, obj) {
  registry.register(obj, id); // heldValue = id, sent to callback on GC
}

let obj = { data: 'important' };
trackObject('obj-1', obj);
obj = null; // Eventually logs: "Object with ID obj-1 was garbage collected"

DOM Element Cache with Auto-Cleanup

class DOMCache {
  #cache = new Map();
  #registry = new FinalizationRegistry((key) => {
    this.#cache.delete(key);
  });

  store(key, element, metadata) {
    this.#cache.set(key, {
      ref: new WeakRef(element),
      metadata,
    });
    this.#registry.register(element, key);
  }

  get(key) {
    const entry = this.#cache.get(key);
    if (!entry) return null;

    const element = entry.ref.deref();
    if (!element) {
      this.#cache.delete(key);
      return null;
    }
    return { element, metadata: entry.metadata };
  }
}

When to Use

Use CaseRecommendation
LRU cache for expensive computationsWeakRef for cache values
Object-to-metadata mappingWeakMap (preferred) or WeakRef
Releasing native/external resourcesFinalizationRegistry
Preventing memory leaks in long-lived mapsWeakRef + FinalizationRegistry
Critical cleanup logicDo not rely on FinalizationRegistry — GC timing is not guaranteed

AbortController

Problem

No standard way to cancel async operations — fetch requests, event listeners, timers, or custom async work.

Basic Usage

const controller = new AbortController();
const { signal } = controller;

// Fetch with cancellation
fetch('/api/data', { signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request was cancelled');
    } else {
      throw err;
    }
  });

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

Timeout Helper

function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  return fetch(url, { ...options, signal: controller.signal })
    .finally(() => clearTimeout(timeout));
}

// Built-in alternative (newer browsers)
fetch('/api/data', { signal: AbortSignal.timeout(5000) });

Cancel Previous Request (Search-as-you-type)

function createSearchHandler(endpoint) {
  let currentController = null;

  return async function search(query) {
    // Cancel previous in-flight request
    currentController?.abort();
    currentController = new AbortController();

    try {
      const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`, {
        signal: currentController.signal,
      });
      return await res.json();
    } catch (err) {
      if (err.name === 'AbortError') return null; // Cancelled, ignore
      throw err;
    }
  };
}

const search = createSearchHandler('/api/search');
input.addEventListener('input', async (e) => {
  const results = await search(e.target.value);
  if (results) renderResults(results);
});

Abort Event Listeners

const controller = new AbortController();

// All listeners removed when controller.abort() is called
document.addEventListener('click', handleClick, { signal: controller.signal });
document.addEventListener('keydown', handleKey, { signal: controller.signal });
window.addEventListener('resize', handleResize, { signal: controller.signal });

// One call removes all three listeners
controller.abort();

Composing Signals

// AbortSignal.any() — abort when ANY signal fires
const userCancel = new AbortController();
const timeout = AbortSignal.timeout(10000);

fetch('/api/data', {
  signal: AbortSignal.any([userCancel.signal, timeout])
});

// Cancels if user clicks cancel OR after 10 seconds
cancelButton.onclick = () => userCancel.abort();

Custom Abortable Operations

async function processItems(items, signal) {
  const results = [];

  for (const item of items) {
    // Check cancellation before each unit of work
    if (signal?.aborted) {
      throw new DOMException('Operation cancelled', 'AbortError');
    }

    const result = await processItem(item);
    results.push(result);
  }

  return results;
}

// Also listen for abort during async work
async function longRunningTask(signal) {
  return new Promise((resolve, reject) => {
    signal?.addEventListener('abort', () => {
      reject(new DOMException('Aborted', 'AbortError'));
    });

    // ... do async work ...
  });
}

Symbols

Problem

No way to create truly unique property keys. No mechanism to customize built-in language behavior for custom objects.

Unique Property Keys

const id = Symbol('id');
const secretId = Symbol('id'); // Different symbol despite same description

const user = {
  name: 'Alice',
  [id]: 12345,
};

user[id];        // 12345
user[secretId];  // undefined — different symbol

// Symbols are hidden from normal enumeration
Object.keys(user);                // ['name']
JSON.stringify(user);             // '{"name":"Alice"}'
Object.getOwnPropertySymbols(user); // [Symbol(id)]

Well-Known Symbols

Customize how objects interact with built-in JavaScript operations:

// Symbol.iterator — make an object iterable
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
}

const range = new Range(1, 5);
[...range];               // [1, 2, 3, 4, 5]
for (const n of range) {} // Iterates 1 through 5
// Symbol.toPrimitive — control type coercion
class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number': return this.amount;
      case 'string': return `${this.amount} ${this.currency}`;
      default: return this.amount;
    }
  }
}

const price = new Money(99.99, 'USD');
+price;          // 99.99
`${price}`;      // "99.99 USD"
price + 1;       // 100.99
// Symbol.hasInstance — customize instanceof
class EvenNumber {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0;
  }
}

4 instanceof EvenNumber;   // true
5 instanceof EvenNumber;   // false

Key Well-Known Symbols

SymbolCustomizesExample
Symbol.iteratorfor...of, spread, destructuringMake objects iterable
Symbol.asyncIteratorfor await...ofAsync iteration
Symbol.toPrimitiveType coercionCustom number/string conversion
Symbol.hasInstanceinstanceofCustom type checks
Symbol.toStringTagObject.prototype.toString()Custom [object Tag] output
Symbol.speciesConstructor for derived objectsControl map(), filter() return type

Symbol.for — Global Symbol Registry

// Shared symbols across modules/iframes/realms
const key = Symbol.for('app.config');
const sameKey = Symbol.for('app.config');

key === sameKey; // true — same symbol from global registry

Symbol.keyFor(key); // 'app.config'

Iterators & Generators

Problem

No lazy evaluation pattern. Processing large datasets requires loading everything into memory. No standard way to produce values on demand.

Custom Iterator

function createPaginator(fetchPage) {
  return {
    [Symbol.asyncIterator]() {
      let page = 1;
      let done = false;

      return {
        async next() {
          if (done) return { done: true };

          const data = await fetchPage(page);
          if (data.length === 0) {
            done = true;
            return { done: true };
          }

          page++;
          return { value: data, done: false };
        }
      };
    }
  };
}

// Usage — processes one page at a time
const pages = createPaginator((page) => fetch(`/api/items?page=${page}`).then(r => r.json()));

for await (const items of pages) {
  renderItems(items);
}

Generators

// Lazy range — produces values on demand, no array allocated
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

for (const n of range(1, 1000000)) {
  if (n > 5) break; // Only generates values 1-6, not all million
}

// Infinite sequence
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Take first 10 Fibonacci numbers
function take(n, iterable) {
  const result = [];
  for (const item of iterable) {
    result.push(item);
    if (result.length >= n) break;
  }
  return result;
}

take(10, fibonacci()); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Async Generators

// Stream processing — read and process chunks as they arrive
async function* readStream(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    yield decoder.decode(value, { stream: true });
  }
}

for await (const chunk of readStream('/api/large-file')) {
  processChunk(chunk);
}
// Polling with backoff
async function* poll(fn, { interval = 1000, maxInterval = 30000, signal } = {}) {
  let delay = interval;

  while (!signal?.aborted) {
    try {
      const result = await fn();
      yield result;
      delay = interval; // Reset on success
    } catch {
      delay = Math.min(delay * 2, maxInterval); // Backoff on failure
    }

    await new Promise(resolve => setTimeout(resolve, delay));
  }
}

// Usage
const controller = new AbortController();

for await (const status of poll(() => fetchStatus(), { signal: controller.signal })) {
  updateUI(status);
  if (status.complete) {
    controller.abort();
  }
}

Generator as State Machine

function* trafficLight() {
  while (true) {
    yield 'green';
    yield 'yellow';
    yield 'red';
  }
}

const light = trafficLight();
light.next().value; // 'green'
light.next().value; // 'yellow'
light.next().value; // 'red'
light.next().value; // 'green' (cycles)

Broadcast Channel

Problem

No simple way to send messages between same-origin tabs, windows, or iframes.

Basic Usage

// Tab 1
const channel = new BroadcastChannel('app-events');

channel.postMessage({
  type: 'USER_LOGOUT',
  timestamp: Date.now(),
});

// Tab 2 (same origin)
const channel = new BroadcastChannel('app-events');

channel.onmessage = (event) => {
  if (event.data.type === 'USER_LOGOUT') {
    // Redirect to login page
    window.location.href = '/login';
  }
};

// Cleanup
channel.close();

Cross-Tab State Sync

class CrossTabState {
  #channel;
  #state;
  #listeners = new Set();

  constructor(channelName, initialState = {}) {
    this.#state = initialState;
    this.#channel = new BroadcastChannel(channelName);

    this.#channel.onmessage = (event) => {
      const { key, value } = event.data;
      this.#state[key] = value;
      this.#listeners.forEach(fn => fn(key, value));
    };
  }

  get(key) {
    return this.#state[key];
  }

  set(key, value) {
    this.#state[key] = value;
    this.#channel.postMessage({ key, value });
    this.#listeners.forEach(fn => fn(key, value));
  }

  onChange(fn) {
    this.#listeners.add(fn);
    return () => this.#listeners.delete(fn);
  }

  destroy() {
    this.#channel.close();
    this.#listeners.clear();
  }
}

// Usage — syncs theme across all tabs
const state = new CrossTabState('app-state', { theme: 'light' });

state.onChange((key, value) => {
  if (key === 'theme') document.documentElement.dataset.theme = value;
});

state.set('theme', 'dark'); // All tabs switch to dark mode

Use Cases

Use CaseExample
Logout syncOne tab logs out → all tabs redirect to login
Theme/locale syncUser changes theme → all tabs update
Cart updatesItem added in one tab → badge updates in all tabs
Leader electionOne tab coordinates background work
Cache invalidationOne tab detects stale data → all tabs refresh

Structured Clone & Transferable Objects

Problem

JSON.parse(JSON.stringify()) cannot handle Date, Map, Set, RegExp, ArrayBuffer, circular references, or undefined.

structuredClone

const original = {
  date: new Date(),
  pattern: /test/gi,
  data: new Map([['key', 'value']]),
  buffer: new ArrayBuffer(8),
  nested: { set: new Set([1, 2, 3]) },
};

// Deep clone preserving all types
const clone = structuredClone(original);

clone.date instanceof Date;      // true
clone.pattern instanceof RegExp; // true
clone.data instanceof Map;       // true
clone.nested.set instanceof Set; // true
clone.date !== original.date;    // true (different reference)

With Transfer

const buffer = new ArrayBuffer(1024 * 1024); // 1MB
// Clone the object but transfer the buffer (zero-copy)
const clone = structuredClone({ data: buffer }, { transfer: [buffer] });

buffer.byteLength;       // 0 — ownership transferred
clone.data.byteLength;   // 1048576

structuredClone vs JSON vs Spread

FeaturestructuredCloneJSON.parse(JSON.stringify())Spread / Object.assign
Deep cloneYesYesNo (shallow)
DatePreservedConverted to stringShared reference
Map / SetPreservedLostShared reference
RegExpPreservedLostShared reference
ArrayBufferPreservedLostShared reference
Circular referencesHandledThrowsShared reference
undefinedPreservedOmittedPreserved
FunctionsNot supportedNot supportedShared reference
DOM nodesNot supportedNot supportedShared reference
Performance (large objects)FastSlower (serialize/parse)N/A (shallow)

Summary

FeatureCore Problem SolvedTypical Scenario
WeakRefMemory leaks from cached referencesObject caches, DOM metadata
FinalizationRegistryResource cleanup after GCRelease native handles, update maps
AbortControllerNo cancellation mechanismFetch, event listeners, async flows
SymbolsNeed unique, non-colliding keysPrivate properties, protocol customization
IteratorsNo lazy, on-demand value productionPagination, large data, streams
GeneratorsComplex iteration logicState machines, infinite sequences
BroadcastChannelCross-tab communicationLogout sync, theme sync, cache invalidation
structuredCloneDeep cloning with type preservationClone complex objects, worker messaging

Best Practices

  • Do not rely on FinalizationRegistry for critical cleanup — GC timing is non-deterministic
  • Always check signal.aborted in long-running operations and listen for the abort event
  • Use Symbol.for() only for intentionally shared symbols — prefer Symbol() for private keys
  • Generators should be used for lazy evaluation, not as a general replacement for functions
  • Close BroadcastChannel when no longer needed to prevent memory leaks
  • Prefer structuredClone over JSON.parse(JSON.stringify()) for deep cloning

On this page