因此,您在示例中所描述的与其说是对代理模式的演示,不如说是对“调用对象”及其在 JavaScript 中的工作方式的混淆演示。
在 JavaScript 中,函数是“一流的”。这实质上意味着函数是数据,就像任何其他数据一样。所以让我们考虑以下情况:
var fn = (function () { return this.x; }),
a = {
x : 1,
fn : fn,
},
x = 2,
nothing = (function (z) { return z; });
所以,我们有一个对象a,它有两个属性:fn 和x。我们还有变量x、fn(这是一个返回this.x的函数)和nothing(它返回传递的任何内容)。
如果我们评估a.x,我们会得到1。如果我们评估x,我们会得到2。很简单吧?现在,如果我们评估nothing(a.x),那么我们得到1。这也很简单。但重要的是要认识到与属性a.x 关联的值1 与对象a 没有任何关系。它独立存在,可以简单地作为值传递。
在 JavaScript 中,函数的工作方式相同。作为属性的函数(通常称为“方法”)可以作为简单引用传递。但是,这样做时,它们可能会与对象断开连接。当您在函数中使用 this 关键字时,这一点变得很重要。
this 关键字引用“调用对象”。这是与函数相关联的对象在评估该函数时。为函数设置调用对象的基本方法有以下三种:
- 如果使用点运算符调用函数(例如
a.fn()),则相关对象(在示例中为a)设置为调用对象。
- 如果使用函数的
call 或apply 属性调用该函数,那么您可以显式设置调用对象(稍后我们将了解为什么这样做很有用)。
- 如果没有通过方法 1 或方法 2 设置调用对象,则使用全局对象(在浏览器中,这通常称为
window)。
那么,回到我们的代码。如果我们调用a.fn(),它将评估为1。这是意料之中的,因为函数中的this 关键字将由于使用点运算符而设置为a。但是,如果我们简单地调用fn(),它将返回2,因为它引用了全局对象的x 属性(意味着使用了我们的全局x)。
现在,事情变得棘手了。如果您致电:nothing(a.fn)(),会怎样?您可能会惊讶于结果是2。这是因为将a.fn 传递给nothing() 传递了对fn 的引用,但不保留调用对象!
这与您的编码示例中的概念相同。如果您的函数handleDrop 使用this 关键字,您会发现它具有不同的值,具体取决于您使用的处理程序形式。这是因为在您的第二个示例中,您传递了对 handleDrop 的引用,但与我们的 nothing(a.fn)() 示例一样,当它被调用时,调用对象引用已丢失。
所以让我们在拼图中添加一些其他内容:
var b = {
x : 3
};
您会注意到,虽然b 具有x 属性(因此满足fn 使用this 的要求),但它没有引用fn 的属性。因此,如果我们想调用fn 函数并将其this 设置为b,似乎我们需要向b 添加一个新属性。但相反,我们可以在fn 上使用上述apply 方法将b 显式设置为调用对象:
fn.apply(b); //is 3
这可用于通过创建新函数“包装器”将调用对象“永久”绑定到函数。它并不是真正的永久绑定,它只是创建一个新函数,用所需的调用对象调用旧函数。这样的工具往往是这样写的:
Function.prototype.bind = function (obj) {
var self = this;
return function() {
return self.apply(obj, arguments);
};
};
所以在执行该代码之后,我们可以执行以下操作:
nothing(a.fn.bind(a))(); //is 1.
这没什么难的。事实上,bind() 属性是内置在 ES5 中的,其工作方式与上面的简单代码非常相似。而我们的bind 代码实际上是一种非常复杂的方法来做一些我们可以做的更简单的事情。由于a 有fn 作为属性,所以我们可以使用点运算符直接调用它。我们可以跳过call 和apply 的所有令人困惑的用法。我们只需要确保当函数被调用时,它是使用点运算符调用的。我们可以在上面看到如何做到这一点,但在实践中,它更简单、更直观:
nothing(function () { return a.fn(); })(); //is 1
一旦您了解了数据引用如何存储在闭包范围内、函数如何成为一等对象以及调用对象如何工作,这一切都变得非常容易理解并且相当直观。
至于“代理”,它们也利用相同的概念来挂钩函数。因此,假设您想计算a.fn 被调用的次数。您可以通过插入代理来做到这一点,就像这样(利用我们上面的bind 代码中的一些概念):
var numCalls = (function () {
var calls = 0, target = a.fn;
a.fn = (function () {
calls++;
return target.apply(a, arguments);
});
return (function () {
return calls;
});
}());
所以现在,无论何时调用numCalls(),它都会返回调用a.fn() 的次数而不实际修改a.fn 的功能!这很酷。但是,您必须记住,您确实更改了 a.fn 引用的函数,所以回顾我们代码的开头,您会注意到 a.fn 不再与 fn 相同,并且可以不再可以互换使用。但现在原因应该很明显了!
我知道这基本上是在几页文本中进行的为期一周的 JavaScript 教育,但这很简单。一旦您理解了这些概念,许多 JavaScript 模式的功能、实用性和威力就会变得非常容易理解。
希望这能让事情变得更清楚!
更新:感谢@pimvdb 指出我对[].slice.call(arguments, 0) 的不必要使用。我删除了它,因为它是不必要的。