What’s a decorator: A function that takes another function and gives it superpowers without changing the original.
The Problem That Led to Decorators:
javascript
function slow(x) {
// Imagine heavy calculation here
console.log(`Computing for ${x}...`);
return x * x;
}
slow(5); // Computing for 5... -> 25
slow(5); // Computing for 5... -> 25 (calculated again!)
slow(5); // Computing for 5... -> 25 (wasted work!)Brain says: “Why recalculate the same thing? Let’s remember results!”
Basic Decorator - Step by Step:
Step 1: Create a wrapper that remembers results
javascript
function cachingDecorator(func) {
let cache = new Map(); // Storage for remembered results
return function(x) {
// Check if we already know the answer
if (cache.has(x)) {
return cache.get(x); // Return saved result
}
// Don't know yet, so calculate it
let result = func(x);
cache.set(x, result); // Remember for next time
return result;
};
}Step 2: Wrap the original function
javascript
slow = cachingDecorator(slow);
slow(5); // Computing for 5... -> 25 (calculated once)
slow(5); // -> 25 (from cache, no computing!)How it flows:
- Call
slow(5) - Wrapper checks cache - not found
- Wrapper calls original
func(5) - Wrapper saves result in cache
- Next
slow(5)finds it in cache, returns immediately
The this Problem - Why Objects Break:
javascript
let worker = {
name: "Worker",
slow(x) {
console.log(`${this.name} computing ${x}`);
return x * x;
}
};
worker.slow(3); // "Worker computing 3" -> 9
// Now decorate it
worker.slow = cachingDecorator(worker.slow);
worker.slow(3); // Error! Cannot read property 'name' of undefinedWhat went wrong:
javascript
// Inside the wrapper, this happens:
let result = func(x); // func is worker.slow, but 'this' is lost!
// It's like doing:
let func = worker.slow; // Extract method from object
func(3); // Call without object - 'this' becomes undefinedSolution: func.call() - Manual this Control:
What func.call() does:
javascript
// Normal call
func(arg1, arg2); // 'this' depends on how it's called
// call() lets you set 'this' manually
func.call(thisValue, arg1, arg2); // 'this' = thisValueFixed decorator:
javascript
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
// Pass along the 'this' from wrapper to original function
let result = func.call(this, x);
cache.set(x, result);
return result;
};
}How the flow works now:
worker.slow(3)- wrapper getsthis = worker- Wrapper calls
func.call(this, x)- passesworkerasthis - Original function runs with correct
this.name
Multiple Arguments - The Next Challenge:
javascript
let worker = {
slow(a, b, c) {
return a + b + c;
}
};
// Our current decorator only handles one argument!
// We need: cache key for (a,b,c) combinationSolution - Universal decorator:
javascript
function cachingDecorator(func, hash) {
let cache = new Map();
return function() { // No parameters - accept anything!
// Create unique key from all arguments
let key = hash(arguments);
if (cache.has(key)) {
return cache.get(key);
}
// Pass ALL arguments to original function
let result = func.call(this, ...arguments);
cache.set(key, result);
return result;
};
}
// Helper to create cache key
function hash(args) {
return args[0] + ',' + args[1] + ',' + args[2]; // Simple joining
}
worker.slow = cachingDecorator(worker.slow, hash);func.apply() - Alternative to call():
Difference between call and apply:
javascript
// call: arguments listed separately
func.call(thisValue, arg1, arg2, arg3);
// apply: arguments in array
func.apply(thisValue, [arg1, arg2, arg3]);Using apply in decorator:
javascript
// Instead of:
let result = func.call(this, ...arguments);
// You can use:
let result = func.apply(this, arguments);Why apply is sometimes better: No need to spread arguments - it takes array-like objects directly.
Method Borrowing Trick:
Problem: arguments isn’t a real array
javascript
function test() {
console.log(arguments.join(',')); // Error! No join method
}Solution: Borrow join from real array
javascript
function hash() {
return [].join.call(arguments); // Borrow join, use on arguments
}
hash(1, 2, 3); // "1,2,3"How borrowing works:
[].joingets the join method from an empty array.call(arguments)runs join withthis = arguments- join doesn’t care if
thisis a real array - just needs indexed properties
Complete Universal Decorator:
javascript
function cachingDecorator(func) {
let cache = new Map();
return function() {
// Create cache key from all arguments
let key = [].join.call(arguments);
if (cache.has(key)) {
return cache.get(key);
}
// Forward everything to original function
let result = func.apply(this, arguments);
cache.set(key, result);
return result;
};
}Bottom line: Decorators wrap functions to add features. Use call/apply to preserve this context. Method borrowing lets you use array methods on array-like objects.
Retry
Your decorator flow is PERFECT:
javascript
function decorator(originalFunc) {
// 1. Outer layer - setup persistent data structure
let cache = new Map(); // or let timeoutId = null for debounce
// 2. Return closure function - the actual wrapper
return function() {
// 3. Inner logic - check data structure first
if (cache.has(key)) {
return cache.get(key); // Use stored data
}
// If not found, call original and store result
let result = originalFunc.apply(this, arguments);
cache.set(key, result);
return result;
};
}Why closure is ESSENTIAL:
javascript
// WRONG - this doesn't work
function badDecorator(func) {
return function() {
let cache = new Map(); // NEW MAP EVERY CALL!
// cache will always be empty...
};
}
// RIGHT - closure preserves data
function goodDecorator(func) {
let cache = new Map(); // PERSISTS between calls
return function() {
// This cache remembers previous calls
};
}Yes, always think 3-layer approach:
- Setup layer - create persistent data
- Closure layer - return the wrapper function
- Logic layer - check data, call original, update data
The this Problem - You NAILED It:
In the worker case:
javascript
let worker = { slow() { return this.name; } };
worker.slow = cachingDecorator(worker.slow);
// Inside decorator:
return function(x) {
let result = func(x); // func is worker.slow extracted
// ↑ This is like: let func = worker.slow; func(x);
}Following the golden rule:
javascript
let func = worker.slow; // Extract method
func(x); // Called without dot → this = undefinedNo object before the dot = no this!
The fix with call():
javascript
return function(x) {
let result = func.call(this, x); // Manually pass 'this'
}
// When called as worker.slow(x):
// - wrapper gets this = worker (because worker.slow())
// - wrapper passes this to original function via call()Visual Flow:
javascript
// Original call
worker.slow(5)
// ↓ this = worker (object before dot)
// Wrapper receives
function(5) { // this = worker here
func.call(this, 5) // Pass worker as 'this' to original
// ↓
}
// Original function gets
slow(5) { // this = worker (thanks to call!)
return this.name; // Works!
}The manual this passing:
worker.slow()gives wrapperthis = worker- Wrapper uses
func.call(worker, ...)to pass it along - Original function receives
this = worker
You understand it perfectly! Decorators = 3-layer closure pattern + manual this forwarding with call/apply.
