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!
Top comments (8)
This becomes more important when you providing context to callbacks. No pun intended.
May you provide any simple example?
My original post was a joke because 'this' is a context sensitive word in English and JavaScript.
Anyways, after thinking about this, the better questions are: When should I modify the scope vs passing a parameter? When should I use a decorator vs a callback? What is the best pattern for attaching behavior?
IMO
this
should not be used outside of classes since arrow functions exist and decorators should probably be preferred over callbacks for comparability purposes.Unfortunately you cannot really see the benefit of decorators unless you use TypeScript.
Both snippets do the same thing. A function is evaluated if some path matches the request path. It all comes down to how/when the code should be accessible.
Although in typescript, you technically do not have to invoke a function, you have more control over data, and you can reuse the wrapper.
Thanks for your comment, appreciate it!
Actually the topic of my post is also valid for decorators syntax as anyway a decorator is a wrapper function and does not pass this inside a decorated function.
Thanks for your comment!
Yeap, exactly, my post covers only functions which are created via function keyword.
Btw, there is one thought from your code example:
If a wrapper function returns arrow function, there is no way to have a transparent calls as this will be shadowed by global object.
P.S. let's assert that out aim is to make a transparent calls and not e real world cases=)
Thanks for your post, it is very useful info!
very nice article. please how did you make your code to display so nice and colourful?
hey, I use that markdown: