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.
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
.
// 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
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.
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.
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.
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.
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.
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
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