A wrapper function (aka a high order function) is a great way to augment a behavior of your other functions while still maintaining a separation of those concerns.
That pattern is widespread in JS/TS world and I consider myself a rather experiences user of it. However, sometime ago I came across a code snippet which demonstrated me that one detail is missed in my self-written wrapper functions.
The code snippet:
function bindActionCreator<A extends AnyAction = AnyAction>(
actionCreator: ActionCreator<A>,
dispatch: Dispatch
) {
return function (this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
}
}
This wrapper function is from Redux library but we may analyze it out of the library context.
So bindActionCreator takes 2 functions as arguments and returns a function which contains a composition of 2 input functions in the body. This is a good example of a utility wrapper function. But there is one place that hooked my attention. Honestly, if I need to create a similar function I would write just actionCreator(args) without apply(...) method call. So, let's find out why the function call via apply method is used in the code snippet.
Let's consider a simple function:
function f(name, email){
return {name, email, this:this}
}
There is just one thing to pay attention, the function f returns object which keeps this value of the function call.
Let's write a simple wrapper function which actually does not add any logic but just returns a new function which contains function f call inside:
function wrap(f) {
return function (...args){
return f(...args)
};
}
Now let's use the wrapper function:
const wrappedF = wrap(f);
f(1,1) // {name: 1, email: 2, this: Window}
wrappedF(1,1) // {name: 1, email: 2, this: Window}
It seems that both calls are equivalent or transparent.
But, let's call the functions like that:
f.apply({a:1}, [1,1]) // {name: 1, email: 2, this: {a:1}}
wrappedF.apply({a:1}, [1,1]) // {name: 1, email: 2, this: Window}
Now results of the calls are not equivalent. Returned objects have different values for this key.
Our wrapper function creates a distortion due to dynamic nature of this object (1). This object only depends on how a function is called and can not be accessed or found via a scope chain. So wrappedF and f functions have the same input arguments but different this objects.
Our initial function wrap can not transfer own this value (which e.g. we can set explicitly via apply method) inside function f. But there is a very simple way to modernize initial wrapper function:
function wrap(f) {
return function (...args){
return f.apply(this, args)
};
}
const wrappedF = wrap(f)
f.apply({a:1}, [1,1]) // {name: 1, email: 2, this: {a:1}}
wrappedF.apply({a:1}, [1,1]) // {name: 1, email: 2, this: {a:1}}
Now our function wrap is transparent. It explicitly passes own this value in function f. Arguments and this are equivalent for the original and the wrapped version.
(1) - dynamic nature of this is true only for non-arrow functions. An arrow function does not have own this value but gets it from a closure (static nature). In case if function f is defined as an arrow function, this value of such function is defined in moment of a creation and already does not depend on how function f is called.
P.S
In my practice I have never met a case where I really need to have a transparent wrapped function, but still it is useful to keep in mind that this can reach and punish you in any time=)
Bless static scope!
This becomes more important when you providing context to callbacks. No pun intended.