JavaScript 回调作用域

58

我在使用普通的JavaScript(没有框架)时,在回调函数中引用我的对象遇到了一些问题。

function foo(id) {
    this.dom = document.getElementById(id);
    this.bar = 5;
    var self = this;
    this.dom.addEventListener("click", self.onclick, false);
}

foo.prototype = {
    onclick : function() {
        this.bar = 7;
    }
};

现在,当我创建一个新对象(在DOM加载后,带有一个span#test)时。
var x = new foo('test');

在onclick函数内部,this指向span#test而不是foo对象。

如何在onclick函数内获取对foo对象的引用?


保存引用:this.cb = this.onclick.bind(this); addEventListener("click", this.cb); - vsync
7个回答

81

(从其他答案的注释中提取了一些解释)

问题出在下面这一行代码:

this.dom.addEventListener("click", self.onclick, false);

在这里,您传递一个函数对象作为回调。当事件触发时,该函数被调用,但是现在它没有与任何对象(this)相关联。

通过将函数(及其对象引用)包装在闭包中,可以解决该问题,如下所示:

this.dom.addEventListener(
  "click",
  function(event) {self.onclick(event)},
  false);

由于闭包创建时将变量self分配为this,因此当在以后的某个时间调用闭包函数时,闭包函数将记住self变量的值。

解决这个问题的另一种方法是创建一个实用函数(并避免使用变量绑定this):

function bind(scope, fn) {
    return function () {
        fn.apply(scope, arguments);
    };
}

更新后的代码将如下所示:

this.dom.addEventListener("click", bind(this, this.onclick), false);

Function.prototype.bind 是 ECMAScript 5 的一部分,提供相同的功能。因此你可以这样做:

this.dom.addEventListener("click", this.onclick.bind(this), false);

对于尚不支持ES5的浏览器,MDN提供以下替代方案:

if (!Function.prototype.bind) {  
  Function.prototype.bind = function (oThis) {  
    if (typeof this !== "function") {  
      // closest thing possible to the ECMAScript 5 internal IsCallable function  
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");  
    }  

    var aArgs = Array.prototype.slice.call(arguments, 1),   
        fToBind = this,   
        fNOP = function () {},  
        fBound = function () {  
          return fToBind.apply(this instanceof fNOP  
                                 ? this  
                                 : oThis || window,  
                               aArgs.concat(Array.prototype.slice.call(arguments)));  
        };  

    fNOP.prototype = this.prototype;  
    fBound.prototype = new fNOP();  

    return fBound;  
  };  
} 

closure()可能是一个更好的名称。 - hishadow
闭包在使用多个对象时不起作用;您将始终引用最后创建的对象。实用函数有效是因为每次都会新存储参数。 - w00t

15
this.dom.addEventListener("click", function(event) {
    self.onclick(event)
}, false);

1
谢谢,但是你能解释一下为什么我需要在那里创建一个匿名函数吗?那为什么会改变绑定呢? - Chris MacDonald
每当事件被触发时,“this”指的是调用该事件的对象。 - Tom
2
当您在问题的表单中使用self.onclick时,它只是一个匿名函数,当处理时,它的工作方式就像您直接将其附加到span一样。当您将其包装在闭包中时,self.onclick(event)确实会执行您想要的操作,例如使用定义闭包上下文中的“self”。 - Adam Bellaire
这仅适用于创建一个对象;请考虑以下代码的输出:function () {var i; for (i=0; i<5; i++) { setTimeout( function() { console.log(i); }, 1000); } }; f(); - w00t
我撤回之前的说法 - 每次调用都会创建一个新的“var self”,并且它不像我之前评论中的示例那样改变。(其中还有一个拼写错误,应该以“function f()”开头) - w00t

5

对于需要解决这个问题的jQuery用户,您应该使用jQuery.proxy


2

这个问题的一个良好解释(我之前有困难理解所描述的解决方案)可以在这里找到。


2
解释是,self.onclick在JavaScript中并不意味着你想象中的意思。它实际上意味着对象self原型中的onclick函数(没有任何方式引用self本身)。
JavaScript只有函数而没有像C#一样的委托,因此不可能将一个方法和应该应用它的对象一起作为回调传递。
唯一的调用回调中的方法的方法是在回调函数内部自己调用它。因为JavaScript函数是闭包,它们能够访问在它们被创建的范围内声明的变量。
var obj = ...;
function callback(){ return obj.method() };
something.bind(callback);

我调用了原型函数“onclick”以便轻松地关联它。我知道实际的DOM onclick事件不会自动调用它,这就是为什么我尝试使用事件侦听器将我的对象的onclick与DOM的onclick函数绑定。 - Chris MacDonald
那不是我的观点,我理解你的onclick函数。我的观点是,在JavaScript中self.onclick和foo.prototype.onclick之间没有区别。在JavaScript中没有办法说“这个方法绑定到这个对象”。 - Vincent Robert
唯一的方法是使用闭包。 - dolmen

1

-3

这是 JS 中最令人困惑的点之一:'this' 变量指向最局部的对象...但函数也是对象,所以 'this' 指向函数。还有其他微妙的点,但我不记得全部了。

通常我避免使用 'this',只需定义一个本地的 'me' 变量并使用它即可。


2
你的陈述完全不准确。一个函数的apply()和call()方法都允许你使用在调用中传递的变量作为'this'来执行函数。 - Kenaniah
1
@Kenaniah:......这样会增加混淆,代码的确切含义取决于它被如何调用! - Javier
JavaScript中的函数非常灵活,不使用this几乎不是解决问题的方法。this是JS中唯一的动态作用域特性,它为语言增加了很多功能。是的,JS中的函数是一等对象,这意味着'执行上下文指针'的动态性质。当明智地使用时,这是一个很棒的特性。 - Arman

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