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:

  1. Call slow(5)
  2. Wrapper checks cache - not found
  3. Wrapper calls original func(5)
  4. Wrapper saves result in cache
  5. 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 undefined

What 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 undefined

Solution: 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' = thisValue

Fixed 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:

  1. worker.slow(3) - wrapper gets this = worker
  2. Wrapper calls func.call(this, x) - passes worker as this
  3. 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) combination

Solution - 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:

  1. [].join gets the join method from an empty array
  2. .call(arguments) runs join with this = arguments
  3. join doesn’t care if this is 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:

  1. Setup layer - create persistent data
  2. Closure layer - return the wrapper function
  3. 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 = undefined

No 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 wrapper this = 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.