I was trying to solve Task#1 from https://javascript.info/call-apply-decorators
It goes like this:
Create a decorator spy(func) that should return a wrapper that saves all calls to function in its calls property.
Every call is saved as an array of arguments.
For instance:
function work(a, b) { alert( a + b ); // 'work' is an arbitrary function or method } work = spy(work); work(1, 2); // 3 work(4, 5); // 9 for (let args of work.calls) { alert( 'call:' + args.join() ); // "call:1,2", "call:4,5" }
The solution is:
function spy(func) { function wrapper(...args) { wrapper.calls.push(args); return func.apply(this, args); } wrapper.calls = []; return wrapper; }
What I do not understand in the given solution (and in Closures in general) is how the property that was assigned to the wrapper(wrapper.calls) is now also available for "work" function.
With the line work = spy(work)
we created wrapper for the "work" function. The value of the latter now is
ƒ wrapper(...args) {
wrapper.calls.push(args);
return func.apply(this, args);
}
Ok, the value of "work" is "wrapper" function. And "wrapper" has .calls property. But how this property is also present with "work"??
It takes time to figure out how function assignments and closures interplay. Doing my best to explain this as of my understanding:
You assign the result of spy(work) to work, so work is now replaced with the wrapper function returned by spy. In JavaScript, functions are objects, so this replacement essentially changes what work references. And it is wrapper function as you returned it from spy(work) call.
Inside spy, a closure is created that holds a reference to the original work function as func. This allows the wrapper function to access and call the original function within its local scope. So, basically the work function you created at first is not erased, you can point to it with different object reference.
The original work function is saved as func in the closure, while work itself now points to wrapper.
By assigning the result of spy to work, you effectively replace work with a new object—the wrapper function—returned from spy.
The new work (now the wrapper function) retains access to the original work function (stored as func in the closure). This allows wrapper to call the original function while keeping track of its own property (calls).
In essence, assigning spy(work) to work means work now points to the wrapper function, which has additional context and properties beyond the original function.
And just like that, you've extended existing function with additional calls property by using closure to preserve the original function and call it within a new function that your return from decorator spy function. The magic in here around the fact that closure preserves original function implementation in its own localized context.