Skip to Content
Learn JS and MdnAdvanced Patterns

JavaScript Memoization, Mixins, Promises, and Proxies

Optimize performance and structure code effectively with advanced JavaScript patterns including memoize, mixin, Proxy, custom iterators, and asynchronous promise handling.

Memoize

Memoization is an optimization technique that caches the result of function calls based on their input arguments. If the same inputs occur again, the cached result is returned instead of recalculating.

This example wraps a basic function (adder) with a memoize utility using a Map and JSON.stringify to store and retrieve results by argument key.

memoize
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.apply(this, args); cache.set(key, result); return result; }; } const adder = function (a, b) { console.log(`Calculating ${a} + ${b}`); return a + b; }; const memoizedAdder = memoize(adder); const result1 = memoizedAdder(3, 4); // Calculating 3 + 4 console.log(result1); // 7 const result2 = memoizedAdder(3, 4); console.log(result2); // 7

Mixins

Mixins allow objects to share reusable behavior without using classical inheritance. In this example, the CanSpeak and CanWalk mixins are applied to different classes using Object.assign, enabling both Person and Robot to share functionality like speak and walk.

mixins

mixins
// Define a mixin for shared behavior const CanSpeak = { speak() { console.log(`${this.name} says: ${this.message}`); }, }; // Define another mixin const CanWalk = { walk() { console.log(`${this.name} is walking.`); }, }; // Create a base class class Person { constructor(name, message) { this.name = name; this.message = message; } } // Apply mixins to the class Object.assign(Person.prototype, CanSpeak, CanWalk); // Create an instance of the class const john = new Person('John', 'Hello, world!'); // Use methods from mixins john.speak(); // Output: John says: Hello, world! john.walk(); // Output: John is walking. // Another example with a different class class Robot { constructor(name, message) { this.name = name; this.message = message; } } // Apply mixins to the Robot class Object.assign(Robot.prototype, CanSpeak, CanWalk); const r2d2 = new Robot('R2D2', 'Beep boop'); r2d2.speak(); // Output: R2D2 says: Beep boop r2d2.walk(); // Output: R2D2 is walking.

Promise Chaining

promise-chaining

This example demonstrates how to chain Promises to perform sequential asynchronous operations. Each .then() receives the result of the previous one, allowing for clean, readable logic with predictable timing — in this case, doubling a number step by step with 1-second delays.

promise chaining
new Promise(function (resolve, reject) { setTimeout(() => resolve(1), 1000); }) .then(function (result) { console.log(result); // 1 return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); }); }) .then(function (result) { console.log(result); // 2 return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); }); }) .then(function (result) { console.log(result); // 4 }); // 1, 2, 4 with 1s between

Promise Error Handling

These examples show how to handle errors in promise chains using .catch(). They demonstrate how synchronous errors thrown during the executor function are automatically caught, while asynchronous errors (e.g., in setTimeout) are not — unless they’re wrapped in a try...catch or returned from a rejected promise.

Understanding where and when errors can be caught is key to building reliable async flows.

rethrow error
new Promise((resolve, reject) => { throw new Error("Whoops!"); }) .catch(function (error) { if (error instanceof URIError) { // handle it } else { console.log("Can't handle such error"); throw error; // throwing this or another error jumps to the next catch } }) .then(function () { /* doesn't run here */ }) .catch((error) => { console.error(`The unknown error has occurred: ${error.message}`); // don't return anything => execution goes the normal way }); // Can't handle such error // The unknown error has occurred: Whoops!

Synchronous vs Asynchronous Errors in Promises

Promises automatically catch synchronous errors thrown inside the executor. However, errors thrown asynchronously (e.g., inside setTimeout) are not caught unless explicitly wrapped in a rejecting Promise or a try...catch block inside async functions.

handling errors
new Promise(function (resolve, reject) { throw new Error("Whoops!"); }).catch((e) => console.error(e.message)); // Whoops! // There’s an “implicit try..catch” around the function code. // So all synchronous errors are handled. // But here the error is generated not while the executor is // running, but later. So the promise can’t handle it. new Promise(function (resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(console.error); // unhandled error ...

Proxy

This example uses a Proxy to intercept property access on an object. When a key is found in the dictionary, the translation is returned; otherwise, the original key is returned as a fallback. Proxies are powerful for defining custom behavior for fundamental operations like get, set, and more.

Proxy
let dictionary = { Hello: "Hola", Bye: "Adiós", }; dictionary = new Proxy(dictionary, { get(target, phrase) { // intercept reading a property from dictionary if (phrase in target) { // if we have it in the dictionary return target[phrase]; // return the translation } else { // otherwise, return the non-translated phrase return phrase; } }, }); // Look up arbitrary phrases in the dictionary! // At worst, they're not translated. console.log(dictionary["Hello"]); // Hola console.log(dictionary["Welcome to Proxy"]); // Welcome to Proxy

Range Iterator

This example demonstrates how to create a custom iterator using the iterator protocol. The makeRangeIterator function generates numbers from start to end with a defined step, manually implementing the next() method. It’s useful for creating lazy sequences or custom iteration logic without relying on generators.

range itarator
function makeRangeIterator(start = 0, end = Infinity, step = 1) { let nextIndex = start; let iterationCount = 0; return { next() { let result; if (nextIndex < end) { result = { value: nextIndex, done: false }; nextIndex += step; iterationCount++; return result; } return { value: iterationCount, done: true }; }, }; } const iter = makeRangeIterator(1, 6, 2); let result = iter.next(); while (!result.done) { console.log(result.value); // 1 3 5 result = iter.next(); } console.log("Iterated over sequence of size:", result.value); // 1, 3, 5 // Iterated over sequence of size: 3

Shooters: Closures & Lexical Scope

Every function is meant to output its number. All shooter functions are created in the lexical environment of makeArmy() function. But when army[5]() is called, makeArmy has already finished its job, and the final value of i is 10 (while stops at i=10). As the result, all shooter functions get the same value from the outer lexical environment and that is, the last value, i=10. Solution is to save variable let j = i

lexical environment(scope)
function makeArmy() { const shooters = []; let i = 0; while (i < 10) { let j = i; // save local variable const shooter = function () { // create a shooter function, return j; // that should show its number }; shooters.push(shooter); // and add it to the array i++; } // ...and return the array of shooters return shooters; } const army = makeArmy(); console.log(army[0]()); // 0 console.log(army[1]()); // 1 console.log(army[5]()); // 5
Last updated on