Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Michal Ševčík
Michal Ševčík

Posted on

     

How to uncurry this

A friend of mine send me a snippet of code and asked me if I could help him see what's going on under the hood. He knew what he can do with it, but was curious (as every developer should be) if understanding the magic behind it would open him a whole lot of new options how to write code.

This is the piece of code:

constuncurryThis=Function.bind.bind(Function.prototype.call);

Do you ever find yourself going through a source code of a library and you stumble upon a piece of code that usesbind(),call(),apply or even their combination, but you just skip to the next line, because it's obviously some sort of black magic?

Well, let's deep dive.

Context, Scope, Execution context

In this article we'll be talking a lot about context, so let's clarify what it is right from the start so there's no confusion as we go along.

In many cases there's a lot of confusion when it comes to understanding whatcontext andscope are. Every function has both scope and context associated to it butthey're not the same! Some developers tend to incorrectly describe one for the other.

Scope

Scope is function based and has to do with the visibility of variables. When you declare a variable inside a function, that variable is private to the function. If you nest function definitions,every nested function can see variables of all parent functions within which it was created. But! Parent functions cannot see variables declared in their children.

// ↖ = parent scope// ↖↖ = grand parent scope// ...constnum_global=10;functionfoo(){// scope has access to:// num_1, ↖ num_globalconstnum_1=1;functionbar(){// scope has access to:// num_2, ↖ num_1, ↖↖ num_globalconstnum_2=2;functionbaz(){// scope has access to:// num_3, ↖ num_2, ↖↖ num_1, ↖↖↖ num_globalconstnum_3=3;returnnum_3+num_2+num_1+num_global;}returnbaz();}returnbar();}console.log(foo());// 16

Context

Context is object based and has to do with the value ofthis within function's body.This is a reference to the object that executed the function. You can also think of a context in a way thatit basically tells you what methods and properties you have access to onthis inside a function.

Consider these functions:

functionsayHi(){return`Hi${this.name}`;}functiongetContext(){returnthis;}

Scenario 1:

constperson_1={name:"Janet",sayHi,getContext,foo(){return"foo";}};console.log(person_1.sayHi());// "Hi Janet"console.log(person_1.getContext());// "{name: "Janet", sayHi: ƒ, getContext: ƒ, foo: ƒ}"

We have created an objectperson_1 and assignedsayHi andgetContext functions to it. We have also created another methodfoo just on this object.

In other wordsperson_1 is ourthis context for these functions.

Scenario 2:

constperson_2={name:"Josh",sayHi,getContext,bar(){return"bar";}};console.log(person_2.sayHi());// "Hi Josh"console.log(person_2.getContext());// "{name: "Josh", sayHi: ƒ, getContext: ƒ, bar: ƒ}"

We have created an objectperson_2 and assignedsayHi andgetContext functions to it. We have also created another methodbar just on this object.

In other wordsperson_2 is ourthis context for these functions.

Difference

You can see that we have calledgetContext() function on bothperson_1 andperson_2 objects, but the results are different. In scenario 1 we get extra functionfoo(), in scenario 2 we get extra functionbar(). It's because each of the functions have different context, i.e. they have access to different methods.

Unbound function

When function is unbound (has no context),this refers to the global object. However, if the function is executed in strict mode,this will default toundefined.

functiontestUnboundContext(){returnthis;}testUnboundContext();// Window object in browser / Global object in Node.js// -- versusfunctiontestUnboundContextStrictMode(){"use strict";returnthis;}testUnboundContextStrictMode();// undefined

Execution context

This is probably where the confusion comes from.

Execution context (EC) is defined as the environment in which JavaScript code is executed. By environment I mean the value of this, variables, objects, and functions JavaScript code has access to, constitutes its environment.

--https://hackernoon.com/execution-context-in-javascript-319dd72e8e2c

Execution context is referring not only to value ofthis, but also to scope, closures, ... The terminology is defined by the ECMAScript specification, so we gotta bear with it.

Call, Apply, Bind

Now this is where things get a little more interesting.

Call a function with different context

Bothcall andapply methods allow you to call function in any desired context. Both functions expect context as their first argument.

call expects the function arguments to be listed explicitly whereasapply expects the arguments to be passed as an array.

Consider:

functionsayHiExtended(greeting="Hi",sign="!"){return`${greeting}${this.name}${sign}`;}

Call

console.log(sayHiExtended.call({name:'Greg'},"Hello","!!!"))// Hello Greg!!!

Notice we have passed the function arguments explicitly.

Apply

console.log(sayHiExtended.apply({name:'Greg'},["Hello","!!!"]))// Hello Greg!!!

Notice we have passed the function arguments as an array.

Bind function to different context

bind on the other hand does not call the function with new context right away, but creates a new function bound to the given context.

constsayHiRobert=sayHiExtended.bind({name:"Robert"});console.log(sayHiRobert("Howdy","!?"));// Howdy Robert!?

You can also bind the arguments.

constsayHiRobertComplete=sayHiExtended.bind({name:"Robert"},"Hiii","!!");console.log(sayHiRobertComplete());// Hiii Robert!

If you doconsole.dir(sayHiRobertComplete) you get:

console.dir(sayHiRobertComplete);// outputƒ bound sayHiExtended()    name: "bound sayHiExtended"    [[TargetFunction]]: ƒ sayHiExtended(greeting = "Hi", sign = "!")    [[BoundThis]]: Object        name: "Robert"    [[BoundArgs]]: Array(2)                0: "Hiii"                1: "!!"

You get back anexotic object that wraps another function object. You can read more aboutbound function exotic objects in the official ECMAScript documentationhere.

Usage

Great, some of you have learned something new, some of you have only went through what you already know - but practice makes perfect.

Now, before we get back to our original problem, which is:

constuncurryThis=Function.bind.bind(Function.prototype.call);

let me present you with a problem and gradually create a solution with our newly acquired knowledge.

Consider an array of names:

constnames=["Jenna","Peter","John"];

Now let's assume you want to map over the array and make all the names uppercased.

You could try doing this:

constnamesUppercased=names.map(String.prototype.toUpperCase);// Uncaught TypeError: String.prototype.toUpperCase called on null or undefined

but thisWILL NOT WORK. Why is that? It's becausetoUpperCase method is designed to be called on string.toUpperCase itself does not expect any parameter.

So instead you need to do this:

constnamesUpperCased_ok_1=names.map(s=>s.toUpperCase());console.log(namesUpperCased_ok_1);// ['JENNA', 'PETER', 'JOHN']

Proposal

So instead of doingnames.map(s => s.toUpperCase()) it would be nice to do, let's say thisnames.map(uppercase).

In other words we need to create a function that accepts a string as an argument and gives you back uppercased version of that string. You could say that we need touncurrythis and pass it explicitly as an argument. So this is our goal:

console.log(uppercase("John"));// Johnconsole.log(names.map(uppercase));// ['JENNA', 'PETER', 'JOHN']

Solution

Let me show you, how can we achieve such a thing.

constuppercase=Function.prototype.call.bind(String.prototype.toUpperCase);console.log(names.map(uppercase));// ['JENNA', 'PETER', 'JOHN']

What has just happened? Let's see whatconsole.dir(uppercase) can reveal.

console.dir(uppercase);// output:ƒ bound call()    name: "bound call"    [[TargetFunction]]: ƒ call()    [[BoundThis]]: ƒ toUpperCase()    [[BoundArgs]]: Array(0)

We got back acall function, but it's bound toString.prototype.toUpperCase. So now when we invokeuppercase, we're basically invokingcall function onString.prototype.toUpperCase and giving it a context of a string!

uppercase==String.prototype.toUpperCase.calluppercase("John")==String.prototype.toUpperCase.call("John")

Helper

It's nice and all, but what if there was a way to create a helper, let's sayuncurryThis, that would accept a function anduncurriedthis exactly like in theuppercase example?

Sure thing!

constuncurryThis=Function.bind.bind(Function.prototype.call);

OK, what has happened now? Let's examineconsole.dir(uncurryThis):

console.dir(uncurryThis);// output:ƒboundbind()name:"bound bind"[[TargetFunction]]:ƒbind()[[BoundThis]]:ƒcall()[[BoundArgs]]:Array(0)

We got back abind function, but withcall function as its context. So when we calluncurryThis, we're basically providing context to thecall function.

We can now do:

constuppercase=uncurryThis(String.prototype.toUpperCase);

which is basically:

constset_call_context_with_bind=Function.bind.bind(Function.prototype.call)constuppercase=set_call_context_with_bind(String.prototype.toUpperCase);

If you know doconsole.dir(uppercase), you can see we end up with the same output as we did inSolution section:

console.dir(uppercase);// output:ƒ bound call()    name: "bound call"    [[TargetFunction]]: ƒ call()    [[BoundThis]]: ƒ toUpperCase()    [[BoundArgs]]: Array(0)

And viola, we now have a utility to unboundthis and pass it explicitly as a parameter:

constuncurryThis=Function.bind.bind(Function.prototype.call);constuppercase=uncurryThis(String.prototype.toUpperCase);constlowercase=uncurryThis(String.prototype.toLowerCase);consthas=uncurryThis(Object.prototype.hasOwnProperty);console.log(uppercase('new york'));// NEW YORKconsole.log(uppercase('LONDON'));// londonconsole.log(has({foo:'bar'},'foo'));// trueconsole.log(has({foo:'bar'},'qaz'));// false

We're done

Thanks for bearing with me to the very end. I hope you have learned something new and that maybe this has helped you understand a little the magic behindcall,apply andbind.

Bonus

Whoever might be interested, here's a version ofcurryThis without usingbind:

functionuncurryThis(f){returnfunction(){returnf.call.apply(f,arguments);};}

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
iamaamir profile image
mak
  • Location
    India
  • Joined

Thats great from the learning perspective but in real world i would hesitate to use this mainly because of readability and for the sake of simplicity

e.g we can have simple utilities

constuppercase=str=>str.toUpperCase()constlowercase=str=>str.toLowerCase();
Enter fullscreen modeExit fullscreen mode

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Frontend & backend web developer. Mostly Javascript, NodeJS, Python, PostgreSQL.
  • Location
    Brno, Czech republic
  • Work
    Fullstack developer
  • Joined

More fromMichal Ševčík

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp