因此,您在示例中所描述的与其说是对代理模式的演示,不如说是对“调用对象”及其在 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
。这也很简单。但重要的是要意识到1
与属性关联的值与a.x
对象没有任何关系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
apply
fn
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
hasfn
作为属性,我们可以使用点运算符直接调用它。我们可以跳过call
and的所有令人困惑的用法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)
. 我已将其删除,因为它是不必要的。