HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•80 Minutes

Advanced JavaScript Patterns

Explore symbols, proxies, reflection, property descriptors, currying, memoization, debouncing, throttling, and metaprogramming.

Symbols and Well-Known Symbols

Symbols provide unique primitive values that can be used as property keys. Two separately created symbols are different even if they have the same description. Well-known symbols such as Symbol.iterator allow JavaScript objects to participate in language protocols.

Symbols are useful when you need non-string property keys or want to integrate with built-in language behavior.

Unique Symbol Keys
const id = Symbol('id');
const user = {
  name: 'Suriya',
  [id]: 123
};

console.log(user[id]);
💡 The symbol becomes a unique property key. Bracket notation is required to access a computed symbol key.
Key Takeaways
  • Symbols are unique primitive values.
  • Symbols can be object property keys.
  • Well-known symbols customize language protocols.
Knowledge Check

What is a key property of Symbol values?

Property Descriptors and Object.defineProperty

Property descriptors control attributes such as writable, enumerable, and configurable for data properties. Accessor properties use get and set instead of a direct value.

Object.defineProperty exposes this lower-level object model and is useful for libraries, polyfills, and controlled APIs.

Non-Writable Property
const config = {};

Object.defineProperty(config, 'version', {
  value: '1.0.0',
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(config.version);
💡 The descriptor creates a property whose value cannot be reassigned through ordinary assignment.
Key Takeaways
  • Property descriptors control property behavior.
  • Data descriptors and accessor descriptors have different fields.
  • defineProperty enables precise object API design.
Knowledge Check

Which descriptor controls whether a data property's value can be changed by assignment?

Proxy and Reflect

A Proxy wraps an object and intercepts operations such as property reads, writes, function calls, and construction. Reflect provides standardized methods for performing many fundamental object operations.

Proxies power advanced abstractions such as reactive systems, validation layers, and virtualization, but they can also make behavior harder to reason about and may introduce performance overhead.

Intercepting Property Access
const user = {
  name: 'Suriya'
};

const proxy = new Proxy(user, {
  get(target, property, receiver) {
    console.log('Reading', property);
    return Reflect.get(target, property, receiver);
  }
});

console.log(proxy.name);
💡 The get trap intercepts property access and delegates the actual operation to Reflect.get.
Key Takeaways
  • Proxy intercepts fundamental object operations.
  • Reflect provides standard mechanisms for performing those operations.
  • Proxies are powerful but should be used intentionally.
Knowledge Check

What is a Proxy primarily used for?

Memoization

Memoization caches the result of a function based on its inputs. It is most useful for deterministic or pure computations where repeated inputs are common and the cost of computing the result is significant.

Memoization has a tradeoff: it exchanges memory for computation. An unbounded memoization cache can itself become a memory problem.

Simple Memoization
function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);

    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const square = memoize(n => n * n);
console.log(square(10));
console.log(square(10));
💡 The second call with the same serialized arguments retrieves the cached result instead of recomputing it.
Key Takeaways
  • Memoization trades memory for computation.
  • It works best for deterministic computations.
  • Cache growth and key design must be considered.
Knowledge Check

What does memoization primarily trade for faster repeated calculations?

Debouncing and Throttling

Debouncing delays execution until a burst of calls stops for a specified period. It is useful for search inputs and resize handling. Throttling limits execution to at most a certain frequency, useful for scroll or pointer events.

The correct choice depends on whether you care about the final event after a burst or continuous periodic updates.

Debounce Utility
function debounce(fn, delay) {
  let timer;

  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const search = debounce(query => {
  console.log('Searching:', query);
}, 300);
💡 Each invocation resets the timer. The function executes only after calls stop arriving for the configured delay.
Key Takeaways
  • Debounce waits for inactivity.
  • Throttle limits execution frequency.
  • Both are useful for high-frequency events.
Knowledge Check

Which technique is best suited to run a search only after the user stops typing briefly?