I think this can be explained more clearly if you work backward.
Context:
Suppose we want to lowercase an array of strings. This can be done like so:
[‘A’, ‘B’].map(s => s.toLowerCase())
Let's say, for whatever reason, I want to make this call more generic. I don't like how s
is bound to this
and the fat arrow is tied to toLowerCase()
.
How about this?
[‘A’, ‘B’].map(String.prototype.toLowerCase)
Well, this doesn't work because map
passes the element as the first argument but String.prototype.toLowerCase
takes no arguments. It expects the input string to be passed as this
.
So a question is can we create a wrapper
function that makes this work?
[‘A’, ‘B’].map(wrapper(String.prototype.toLowerCase))
wrapper
returns a function that turns the first argument passed into this
for String.prototype.toLowerCase
to use.
I claim that your uncurryThis === wrapper
.
Proof:
So let's not try to understand unCurryThis
all at once. Instead, let's use some formulas to transform unCurryThis
into something more understandable.
Some formulas first:
instance.function(...args)
=== (instance.constructor.prototype).function.call(instance, ...args)
=== (Class.prototype).function.call(instance, ...args) [1]
=== (Class.prototype).function.bind(instance)(...args) [2]
For example,
Class === String
instance === 'STRING'
function === toLowerCase
args === []
---
'string'.toLowerCase()
=== ('STRING'.constructor.prototype).toLowerCase.call('STRING')
=== (String.prototype).toLowerCase.call('STRING')
=== (String.prototype).toLowerCase.bind('STRING')()
So let's just blindly apply these formulas without worrying about what the confusing uncurryThis
looks like:
'string'
=== (wrapper)(String.prototype.toLowerCase)('STRING')
=== (uncurryThis)(String.prototype.toLowerCase)('STRING')
=== (Function.bind.bind(Function.call))(String.prototype.toLowerCase)('STRING')
// Function.bind is not really the generic form because it's not using the prototype
// Here Function is an instance of a Function and not the constructor.prototype
// It is similar to calling Array.bind or someFunction.bind
// a more correct version would be
// someFunction.constructor.prototype.bind === Function.prototype.bind, so
=== (Function.prototype.bind.bind(Function.prototype.call))(String.prototype.toLowerCase)('STRING')
// Apply formula 2
// instance.function(...args) === (Class.prototype).function.bind(instance)(...args) [2]
// Class === Function
// function === bind
// instance === Function.prototype.call
// ...args === String.prototype.toLowerCase
=== instance.function(...args)('STRING')
=== (Function.prototype.call).bind(String.prototype.toLowerCase)('STRING')
// Apply formula 2 again
// Class == Function
// function == call
// instance === String.prototype.toLowerCase
// ...args === 'STRING'
=== instance.function(...args)
=== (String.prototype.toLowerCase).call('STRING')
// Apply formula 1
instance.function(...args) === (Class.prototype).function.call(instance, ...args) [1]
// Class === String
// function === toLowerCase
// instance === 'STRING'
// args === []
=== instance.function(...args)
=== 'STRING'.toLowerCase(...[])
=== 'STRING'.toLowerCase()
// So we have
(wrapper)(String.prototype.toLowerCase)('STRING')
=== (uncurryThis)(String.prototype.toLowerCase)('STRING')
=== 'STRING'.toLowerCase()
=== 'string'
Reverse Proof:
So you might wonder "how did the guy even derive the uncurryThis
function"?
You can reverse the proof to derive it. I am just copying the equations from above but reversed:
'STRING'.toLowerCase()
=== (String.prototype.toLowerCase).call('STRING') // apply formula [1]
=== (Function.prototype.call).bind(String.prototype.toLowerCase)('STRING') // apply formula [2]
// At this point, you might wonder why `uncurryThis !== (Function.prototype.call).bind)
// since it also takes (String.prototype.toLowerCase)('STRING')
// This is because passing in (Function.prototype.call).bind) as an argument
// is the same as passing in Function.prototype.bind
// `this` binding isn't done unless you call
// (Function.prototype.call).bind)(String.prototype.toLowerCase)
// at that exact moment.
// If you want to be able to pass unCurryThis as a function, you need to bind the
// Function.prototype.call to the Function.prototype.bind.
=== (Function.prototype.bind.bind(Function.prototype.call))(String.prototype.toLowerCase)('STRING') // apply formula 2
=== (Function.bind.bind(Function.call))(String.prototype.toLowerCase)('STRING') // un-generic-ize
=== (uncurryThis)(String.prototype.toLowerCase)('STRING')
=== (wrapper)(String.prototype.toLowerCase)('STRING')
=>
unCurryThis === wrapper === Function.bind.bind(Function.call)
Still pretty confusing to follow but try to write out what Class
, function
, instance
, and args
are each time I am applying formulas [1] and [2], and it should make sense.
unbind
" – MyrtieFunction.call === f.call === Function.prototype.call
– Loveless