DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Cover image for Wrap a JS function transparently
Andrey Smolko
Andrey Smolko

Posted on • Updated on

Wrap a JS function transparently

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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)
    };
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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 (9)

Collapse
lukeshiru profile image
Luke Shiru

Or you could simply avoid this and be a happier developer ๐Ÿคฃ ... I was about to mention that using arrow functions you shouldn't have any issues, but you mentioned that yourself at the end of the article. For those wondering how that looks like:

const f = (name, email) => ({ name, email, this: this });

const wrap =
    f =>
    (...args) =>
        f(...args);

const wrappedF = wrap(f);

f.apply({ a: 1 }, [1, 1]); // { name: 1, email: 2, this: globalThis }
wrappedF.apply({ a: 1 }, [1, 1]); // { name: 1, email: 2, this: globalThis }
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
smlka profile image
Andrey Smolko Author

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:

function f(name, email){
   return {name, email, this:this}
}

const wrap = f => (...args) => 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: globalThis }
Enter fullscreen mode Exit fullscreen mode

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=)

Collapse
wadecodez profile image
Wade Zimmerman

This becomes more important when you providing context to callbacks. No pun intended.

Collapse
smlka profile image
Andrey Smolko Author • Edited on

May you provide any simple example?

Collapse
wadecodez profile image
Wade Zimmerman • Edited on

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.

// Plain JavaScript

function Request () {
    this.path = "/users"
}

function Route () {}

Route.get = function (path) {
    return function(handler) {
    const req = new Request()
    return path === req.path ? handler.apply({req}, [req]) : null
  }
}

Route.get('/users')(function (req) {
    console.log(this, req)
})
Enter fullscreen mode Exit fullscreen mode
//TypeScript

class Request {
    path = "/users"
}

class Route {
    static get(path:string) {
        return function(target:any, propertyKey: string, descriptor: PropertyDescriptor) {
                const req = new Request()
                return path === req.path ? target[propertyKey].apply({req}, [req]) : null;
        }
    }
}

class Controller {
    @Route.get('/users')
    @Route.get('/users/all')
    public getUsers (req: Request) {
        console.log("hello users", this, req)
    }
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
smlka profile image
Andrey Smolko Author

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.

Collapse
paloolap profile image
paloOlap

Thanks for your post, it is very useful info!

Collapse
xenxei46 profile image
Wisdom John ikoi

very nice article. please how did you make your code to display so nice and colourful?

Collapse
smlka profile image
Andrey Smolko Author

hey, I use that markdown:

Image description

๐Ÿค” Did you know?

ย 
๐ŸŽ™ DEV hosts some podcasts that you can find on our Podcasts page.