【问题标题】:JavaScript Proxy Pattern ExplainedJavaScript 代理模式解释
【发布时间】:2012-09-06 21:44:02
【问题描述】:

我学习了 JavaScript 代理模式,但我仍然没有得到,我可以从中受益。因此,我想为您提供两个示例,并请您指出它们之间的区别。

请看下面的代码:

  • 这两个addEventListener 调用有什么区别?其中一位以常规方式致电handleDrop。另一个使用代理模式。
  • 使用代理模式方法可以获得什么?

我测试了这两个函数,它们都成功调用了handleDrop

DndUpload.prototype.buildDropZone = function ()
{
    var self = this,

    this.dropZone.addEventListener('drop', function (e) { self.handleDrop.call(self, e) }, false);
    this.dropZone.addEventListener('drop', self.handleDrop, false);


    DndUpload.prototype.handleDrop = function (e)
    {
        alert("test");
        ...
    };
}

您可以为我提供很好的参考,其中包含对 JavaScript 中代理模式的非常清晰的解释。

【问题讨论】:

  • 试试console.log(this)而不是你的警报,它应该会变得更清楚。

标签: javascript dom-events proxy-pattern


【解决方案1】:

因此,您在示例中所描述的与其说是对代理模式的演示,不如说是对“调用对象”及其在 JavaScript 中的工作方式的混淆演示。

在 JavaScript 中,函数是“一流的”。这实质上意味着函数是数据,就像任何其他数据一样。所以让我们考虑以下情况:

var fn = (function () { return this.x; }),
    a = {
        x : 1,
        fn : fn,
    },
    x = 2,
    nothing = (function (z) { return z; });

所以,我们有一个对象a,它有两个属性:fnx。我们还有变量xfn(这是一个返回this.x的函数)和nothing(它返回传递的任何内容)。

如果我们评估a.x,我们会得到1。如果我们评估x,我们会得到2。很简单吧?现在,如果我们评估nothing(a.x),那么我们得到1。这也很简单。但重要的是要认识到与属性a.x 关联的值1 与对象a 没有任何关系。它独立存在,可以简单地作为值传递。

在 JavaScript 中,函数的工作方式相同。作为属性的函数(通常称为“方法”)可以作为简单引用传递。但是,这样做时,它们可能会与对象断开连接。当您在函数中使用 this 关键字时,这一点变得很重要。

this 关键字引用“调用对象”。这是与函数相关联的对象在评估该函数时。为函数设置调用对象的基本方法有以下三种:

  1. 如果使用点运算符调用函数(例如a.fn()),则相关对象(在示例中为a)设置为调用对象。
  2. 如果使用函数的callapply 属性调用该函数,那么您可以显式设置调用对象(稍后我们将了解为什么这样做很有用)。
  3. 如果没有通过方法 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 代码实际上是一种非常复杂的方法来做一些我们可以做的更简单的事情。由于afn 作为属性,所以我们可以使用点运算符直接调用它。我们可以跳过callapply 的所有令人困惑的用法。我们只需要确保当函数被调用时,它是使用点运算符调用的。我们可以在上面看到如何做到这一点,但在实践中,它更简单、更直观:

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) 的不必要使用。我删除了它,因为它是不必要的。

【讨论】:

  • +1,嗨皮特。你的回答令人震惊。谢谢你写了这么多信息。我必须重新阅读它几次,我需要获得一些练习。但是,您以最好的方式解释了它。这些例子很棒。我仍然想保持这个线程开放几天,看看是否有人在这里放了一些其他信息。但您的帖子是已接受答案状态的候选人#1。谢谢。
  • 这是一个非常不错的解释。我只是想补充一点,[].slice.call 不是必需的 - 您可以将 arguments 传递给 .apply
  • @pimvdb 我不确定,而且我在没有实际测试我的代码的情况下做了大部分工作(我知道,这很淘气)。所以我玩得很安全。您永远不知道在任何给定情况下“数组式”arguments 是如何的。我会更新修复(一旦我确认你是正确的)。 :-)
【解决方案2】:

考虑下面的代码:

function printThis() {
  console.log(this);
}

var someObject = {
  performTest : function() {
    var self = this;

    someOtherObject.higherOrderFunction(printThis);
    someOtherObject.higherOrderFunction(function(){printThis.call(self)});
  }
}

var someOtherObject = {
  higherOrderFunction : function(f) {
      f();
  }
}

someOtherObject.higherOrderFunction(printThis) 会返回什么? 怎么样someOtherObject.higherOrderFunction(function(){printThis.call(self)})

第一个问题的答案取决于您调用 someObject.performTest() 的对象和方式。如果我只是从全局上下文中调用 someObject.performTest(),它可能会打印 Window

无论如何,第二个总是会打印someObject 实例。

当您想要精确控制函数的执行上下文时,您所称的闭包或“代理模式”会派上用场。

注意:this 在 javascript 中的行为与在其他语言中的行为不同(例如在 Java 中)。

【讨论】:

  • +1,您好,感谢您的回答。如果我理解正确的话,当作为标准调用 someOtherObject.higherOrderFunction(printThis); 与代理函数 someOtherObject.higherOrderFunction(function(){printThis.call(self)}); 调用时,这是不同的。然而,这对我来说是很合乎逻辑的,因为代理函数function(){printThis.call(self) 包含在父函数中。但是,它的实际好处在哪里?我为什么要使用代理模式?
  • 当你想确保你刚刚传递给higherOrderFunction的函数以self作为'this'执行时是有益的。否则,“this”将根据 highOrderFunction 的调用方式而有所不同。
  • 啊,另一个好处是如果我希望使代码保持一致。有时,this 可能会有所不同,尤其是当函数是 CALLBACK 时。但是,如果我希望确保该函数仅作为一个特定对象的成员被调用,那么使用代理模式是一个很好的理由。
  • 嗯.. 但是无论如何,我仍然可以调用一个函数,例如:self.handleDrop。这样,我可以确保该函数将是 self 的成员,这是我的特定且不会更改的父对象。
  • 正如 pimvdb 添加的那样,您可以在调用代理中的实际函数之前做很多有用的事情。您可以在调用实际函数之前进行日志记录、检测、添加其他处理等。
【解决方案3】:

基本上,直接传递self.handleDrop在功能上等同于传递以下函数:

function() {
  return self.handleDrop.apply(this, arguments);
}

因为一切都传递给原始函数:

  • this
  • 论据
  • 返回值

考虑到这一点,如下比较你的函数:

function(e) { self.handleDrop.call(self, e) }
function() { return self.handleDrop.apply(this, arguments); }

与你的代理方式不同的是:

  • 它不传递返回值。
  • 它不会传递所有参数(只有第一个,e
  • 它不会传递this 值,而是使用预定义的值:self

现在,前两项在这里没有什么区别,因为addEventListener 不关心返回值,而且它也只传递一个参数。

但第三项很重要:它在函数中设置了不同的this 值。默认情况下,this 是您将事件绑定到的元素(由浏览器设置)。使用代理方式,可以设置另一个this值。

现在,在您的 sn-p 中,尚不清楚为什么每次调用 buildDropZone 时都要设置原型函数。通常你只定义一次原型函数。但是当你的处理程序handleDrop使用代理方式调用时,this指的是DndUpload实例,这与原型函数大体上是一致的。

【讨论】:

  • +1,您好,非常感谢您的解释。它确实非常好。总而言之,代理模式让我对输入函数的变量多了一层控制。在将变量输入函数之前,可以稍微推迟函数的执行,并且可以通过代理函数对变量进行预处理。我理解正确吗?
  • @Bunkai.Satori:是的,这是一个很好的描述。代理函数接收事物(this,参数)并可以在调用原始函数之前/之后修改它们。
猜你喜欢
  • 2011-09-25
  • 2016-06-29
  • 1970-01-01
  • 1970-01-01
  • 2011-11-14
  • 2012-04-07
  • 2014-01-11
  • 2015-07-31
  • 1970-01-01
相关资源
最近更新 更多