Function.bind.bind(Function.call)如何去柯里化?

14

我们的代码库中有这样一行代码:

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

我正在努力解决的问题。大概是将其拆分开来。我该怎么做才能解决这个问题?

我猜这是Function.bind的一个版本,它自己的this被绑定到了Function.call。但这并没有帮助我太多。而且我也没找到任何用途,所以我甚至不确定你是要单独调用它还是需要首先“将其绑定为方法”,你懂的,先绑定再调用。


5
谢谢你让我头疼。:P - Matt Ball
2
@p.s.w.g 他在 Google 找到了一份工作。 - djechlin
@djechlin 我碰巧发现了这个jsPerf。它可能会有用。 - p.s.w.g
1
我认为在JavaScript上下文中使用“curry”或“uncurry”这个术语通常是不准确的;在这种情况下,我不确定它的意图是什么。 - Pointy
这是一个提示,对我来说非常有帮助:Function.call === f.call === Function.prototype.call - Nathan Chappell
显示剩余4条评论
4个回答

16

它将call函数传递给bind函数,其中bind函数本身是this的值。因此,当你调用它时,你会得到一个包装bind函数的对象,它会安排thiscall函数。那个又是一个函数,让你可以创建一个围绕你传递给它的参数绑定的call函数的包装器。

假设你从今天早上起就没有持续饮用咖啡,那么我们来一步一步地看:

  • Function.bind.bind是对bind函数的引用。该引用是从bind函数本身的属性 — 令人困惑的点1 — 生成的。请记住,当使用某些函数作为对象调用bind函数时,它用于创建一个围绕该函数的包装器,并将this绑定到传递的第一个参数。
  • 因此,该函数调用会返回一个函数。该函数的作用类似于调用Function.call.bind(something)
  • 如果你将某个随机函数作为参数传递给那个函数,那么你将得到一个包装随机函数的对象,当调用它时,它会像randomFunction.call(whatever)一样工作。

因此:

function random() {
  alert(this.foo);
}

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

var randomcall = bb(random);

randomcall({ foo: "hello world" }); // alerts "hello world"

最终的要点在于:你有一个函数,函数内部有些代码期望 this 有一些属性,并且会以某种方式使用 this。你希望能够在这里或那里使用该函数与一些对象配合使用。显然,你可以这样做。

random.call(someObject);

不过这个神奇的"绑定-绑定-调用"技巧为你提供了一种廉价的方法来创建一个变体函数,让你避免显式地编写.call()的调用。它还可以让你在前端高级开发者的职位上多待一会儿。

编辑 - 我要透露上面那个笑话的结尾,因为我想到了一个使用绑定+调用技巧的好理由,以获取一个函数,该函数安排调用某个期望通过this在一些"拥有者"对象上操作的所需函数。假设你有一个字符串数组,并且你想得到这些字符串的小写版本。你可以这样写:

var uc = ["Hello", "World"];
var lc = uc.map(function(s) { return s.toLowerCase(); });

但是,有了神奇的“bb”函数,我们也可以这样写:

var uc = ["Hello", "World"];    
var tlc = bb(String.prototype.toLowerCase);
var lc = uc.map(tlc);

这样写并没有多大改进,但如果我们为所有方便的String原型方法制作一组bb()封装,可能会更有意义。当然,万物都有代价,这些包装器很可能会影响性能。(如果这样的做法很常见,那么运行时可能会得到改善。)


我因你而喷出了咖啡。有没有什么方法可以关注你并获取更多的编程幽默? - pilau
@pilau 哈哈,谢谢你找到了这个;我前几天也在找它,但是找不到了 :) 我通常不那么幽默! - Pointy
你有博客或者其他什么东西吗?拥有这么高的声望分数的人一定有博客吧! - pilau
@pilau 我有,但是我太懒得写了。虽然前几天有些事情想写,也许等我有精力了就会写吧。下周我要去参加一个会议,也许到时候我会写一些东西 :) (哦,我的博客链接在个人资料中。) - Pointy
祝您在会议上度过愉快的时光!同时,这可能与此相关:https://dev59.com/WWEi5IYBdhLWcg3wMqBy#21792913 - pilau

6

好的。你知道 bind 是什么吗?它是函数的一个方法,用于绑定它们的this参数,并返回一个新的函数。它可以简化为:

function bind(context) {
    var fn = this;
    return function() {
        return fn.apply(context, arguments);
    };
}

我将使用更多的部分应用程序,以更加函数式的方式缩写带有上下文的函数调用:bindfn(context) -> fncontext。带参数时:(bindfn(context))(…) 等同于 fncontext(…)。
类似地,call 接受一个 this 值,但它不像返回一个函数,而是立即应用它:callfn(context, …) -> fncontext(…)。
现在让我们看看你的代码:bind.call(bind, call)。在这里,你正在将 bind 应用于 bind,并将 call 作为 this 值:bindbind(call)。让我们扩展此内容(按照上述规则),以得到 bindcall。如果现在我们向其提供了一些参数呢?

bindbind(call) (fn)(context, …)

bindcall (fn)(context, …)

call fn(context, …)

fncontext(…)

逐步进行,我们可以这样做:

uncurryThis = bindbind(call) // bindcall

func = uncurryThis(method) // callmethod

result = func(context, …) // methodcontext(…)

这个方法的一个实际用例是任何应该被转换为静态函数(以对象作为第一个参数调用方法)的 "类" 方法。
var uncurryThis = Function.bind.bind(Function.call);
var uc = uncurryThis(String.prototype.toUpperCase);
uc("hello") // in contrast to "hello".toUpperCase()

如果您无法调用方法但需要静态函数,则可以使用这个操作。例如:

["hello", "world"].map(uc) // imagine the necessary function expression

此外,您想调用的方法可能不是对象本身的方法,例如:
var slice = uncurryThis(Array.prototype.slice);
slice(arguments) // instead of `Array.prototype.slice.call(arguments)` everywhere

如果有帮助的话,这里还有一个明确的实现,没有任何绑定:
function uncurryThis(method) {
    return function(context/*, ...*/)
        return method.apply(context, Array.prototype.slice.call(arguments, 1));
    };
}

1

如果你倒着思考,我认为这个问题可以更清晰地解释。

背景:

假设我们想要将一个字符串数组转换为小写。可以像下面这样实现:

[‘A’, ‘B’].map(s => s.toLowerCase())

假设出于任何原因,我想让这个调用更加通用化。我不喜欢s绑定到this,并且箭头函数与toLowerCase()绑定。

这样怎么样?

[‘A’, ‘B’].map(String.prototype.toLowerCase)

这样做不起作用,因为map将元素作为第一个参数传递,但String.prototype.toLowerCase不需要参数。它期望输入字符串作为this传递。

那么问题是,我们能否创建一个wrapper函数来使其工作?

[‘A’, ‘B’].map(wrapper(String.prototype.toLowerCase))

wrapper 返回一个函数,该函数将传递的第一个参数转换为 this,以供 String.prototype.toLowerCase 使用。

我声称你的 uncurryThis === wrapper


证明:

所以,让我们不要试图一次性理解unCurryThis。相反,让我们使用一些公式将unCurryThis转换为更容易理解的形式。

首先是一些公式:

instance.function(...args)
=== (instance.constructor.prototype).function.call(instance, ...args)
=== (Class.prototype).function.call(instance, ...args) [1]
=== (Class.prototype).function.bind(instance)(...args) [2]

例如,
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')()

所以让我们不必担心令人困惑的uncurryThis是什么样子,只需盲目地应用这些公式:

'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'

反向证明:

你可能会想,“这个人怎么推导出uncurryThis函数的呢?”

你可以通过反向证明来推导它。我只是从上面复制方程式,但是反过来:

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

虽然仍然很难理解,但每次应用公式[1]和[2]时,尝试写出函数实例参数,这样就会有意义了。


0
当我们在函数上调用bind时,它会返回一个新的函数,其中this被替换为上下文:
function random() {
  alert(this.foo);
}
var newRandom = random.bind({foo:"hello world"}) //return new function same as //`random` with `this` is replaced by object {foo:"hello world"}

我们有相同的东西:

Function.bind.bind(Function.call)
// return new Function.bind with its `this` is replaced by `Function.call`

它具有以下源代码(使用@Bergi提供的bind函数的简化版本):
var bb = function bind(context){
  var fn = Function.call;
  return function() {
        return Function.call.apply(context, arguments); //also replace fn here for easier reading
    };
}

请注意这里的上下文是函数,例如random,所以当我们调用bb(random)时,我们有一个名为newRandom的函数,如下所示:
newRandom = function(){
   return Function.call.apply(random, arguments); //also replace 
}
//`apply` function replace `this` of Function.call to `random`, and apply Function(now become `random`) with arguments in `arguments` array.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接