How does Function.bind.bind(Function.call) uncurry?

前端 未结 4 1018
陌清茗
陌清茗 2020-12-08 17:11

We have this line in my code base:

var uncurryThis = Function.bind.bind(Function.call);

That I\'m trying to work through. Presumably, it un

4条回答
  •  天命终不由人
    2020-12-08 17:41

    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.

提交回复
热议问题