在JavaScript原型函数中保留对"this"的引用

94

我刚开始使用原型式JavaScript,但是在作用域改变时,我无法弄清如何保留对主对象的this引用。让我举个例子(这里我使用了jQuery):

MyClass = function() {
  this.element = $('#element');
  this.myValue = 'something';

  // some more code
}

MyClass.prototype.myfunc = function() {
  // at this point, "this" refers to the instance of MyClass

  this.element.click(function() {
    // at this point, "this" refers to the DOM element
    // but what if I want to access the original "this.myValue"?
  });
}

new MyClass();

我知道可以通过在myfunc函数的开头执行以下操作来保留对主对象的引用:

var myThis = this;

然后使用myThis.myValue来访问主对象的属性。但是如果我在MyClass上有很多原型函数,那么我是否需要在每个函数开始时保存对this的引用呢?看起来应该有一种更简洁的方式。还有像这样的情况:

MyClass = function() {
  this.elements $('.elements');
  this.myValue = 'something';

  this.elements.each(this.doSomething);
}

MyClass.prototype.doSomething = function() {
  // operate on the element
}

new MyClass();
在这种情况下,我无法使用var myThis = this;创建对主对象的引用,因为即使在doSomething上下文中,this的原始值也是jQuery对象而不是MyClass对象。
有人建议我使用全局变量来保存对原始this的引用,但我觉得那似乎是一个很糟糕的想法。我不想污染全局名称空间,并且似乎会阻止我实例化两个不同的MyClass对象而不会干扰彼此。
有什么建议吗?有没有一种干净的方式来做我想要的事情?还是我的整个设计模式有缺陷?
7个回答

66

为了保留上下文,bind方法非常有用,现在它已经成为最近发布的ECMAScript第五版规范的一部分,此函数的实现很简单(只有8行代码):

// The .bind method from Prototype.js 
if (!Function.prototype.bind) { // check if native implementation available
  Function.prototype.bind = function(){ 
    var fn = this, args = Array.prototype.slice.call(arguments),
        object = args.shift(); 
    return function(){ 
      return fn.apply(object, 
        args.concat(Array.prototype.slice.call(arguments))); 
    }; 
  };
}

你可以像这样在你的例子中使用它:

MyClass.prototype.myfunc = function() {

  this.element.click((function() {
    // ...
  }).bind(this));
};

另一个例子:

var obj = {
  test: 'obj test',
  fx: function() {
    alert(this.test + '\n' + Array.prototype.slice.call(arguments).join());
  }
};

var test = "Global test";
var fx1 = obj.fx;
var fx2 = obj.fx.bind(obj, 1, 2, 3);

fx1(1,2);
fx2(4, 5);

在这个第二个例子中,我们可以更加详细地观察到bind的行为。

bind基本上生成了一个新函数,该函数将负责调用我们的函数并保留函数上下文(this值),该上下文定义为bind的第一个参数。

其余的参数简单地传递给我们的函数。

请注意,在此示例中,函数fx1是在没有任何对象上下文obj.method())的情况下调用的,就像一个简单的函数调用一样。 在此类调用中,内部的this关键字将引用全局对象,它将警报“global test”。

现在,fx2bind方法生成的新函数,它将调用我们的函数并保留上下文并正确传递参数。 它将警报“obj test 1, 2, 3, 4, 5”,因为我们添加了另外两个参数来调用它,而它已经绑定了前三个参数。


2
我强烈建议坚持使用 Function.prototype.bind 这个名称。它已成为语言的标准化部分,不会消失。 - bobince
1
@bobnice:完全同意,原生实现很快将在主要的JavaScript引擎中提供... https://bugs.webkit.org/show_bug.cgi?id=26382 https://bugzilla.mozilla.org/show_bug.cgi?id=429507 - Christian C. Salvadó
3
了解浏览器漏洞情况很重要。顺便说一下,jQuery 1.4现在包括了 jQuery.proxy,它具有类似(但不完全相同)的功能。使用方法如下:$.proxy(obj.fx, obj)或者$.proxy(obj, "fx") - Rob Van Dam
1
这是JS的一个很大的缺点。在大型项目中,它会使代码看起来很混乱。闭包的概念是JS引入的最糟糕的想法之一。似乎无法将对象原型的上下文绑定到实际对象。 - luke1985
这比实际需要的更加复杂。无论如何+1,但我找到了一个更简单的解释。我的问题是将回调作为字符串传递给原型函数。为了使该回调正确地使用“this”,我必须在将其用作回调时将“this”绑定到原型函数。例如:if(callback){var func = this [callback];如果(typeof func ==='function'){var thefunc = func.bind(this);thefunc();}} - Lazerblade
显示剩余2条评论

10

对于您最后的MyClass示例,您可以这样做:

var myThis=this;
this.elements.each(function() { myThis.doSomething.apply(myThis, arguments); });

在传递给each的函数中,this指代一个jQuery对象,这一点你已经知道了。如果在这个函数内部从myThis获取doSomething函数,然后使用参数数组调用该函数的apply方法(参见apply函数arguments变量),那么this将被设置为doSomething中的myThis


1
那样做不行,当你到达this.doSomething时,this已经被jQuery替换为其中一个元素。 - Rob Van Dam
是的,我最初发布时有两个问题。我进行了编辑,现在应该可以工作了。(对此很抱歉...) - icktoofay

8

我知道这是一个旧的帖子,但我有一个更加优雅的解决方案,除了它通常不被采用,我还没有发现其他缺点。

请考虑以下内容:

var f=function(){
    var context=this;
}    

f.prototype.test=function(){
    return context;
}    

var fn=new f();
fn.test(); 
// should return undefined because the prototype definition
// took place outside the scope where 'context' is available

在上面的函数中,我们定义了一个局部变量(context)。然后添加了一个原型函数(test),该函数返回局部变量。正如您可能预测的那样,当我们创建此函数的实例并执行测试方法时,它不会返回局部变量,因为当我们将原型函数定义为主函数的成员时,它在定义局部变量的作用域之外。
这是创建函数然后向其中添加原型的一般问题-您无法访问在主函数作用域内创建的任何内容。
要创建在局部变量范围内的方法,我们需要直接将它们定义为函数的成员,并且摆脱原型引用:
var f=function(){
    var context=this;    

    this.test=function(){
        console.log(context);
        return context;
    };
}    

var fn=new(f);
fn.test(); 
//should return an object that correctly references 'this'
//in the context of that function;    

fn.test().test().test(); 
//proving that 'this' is the correct reference;

你可能担心由于方法没有通过原型方式创建,不同的实例可能并不真正分离数据。为了证明它们是分离的,请考虑以下内容:

var f=function(val){
    var self=this;
    this.chain=function(){
        return self;
    };    

    this.checkval=function(){
        return val;
    };
}    

var fn1=new f('first value');
var fn2=new f('second value');    

fn1.checkval();
fn1.chain().chain().checkval();
// returns 'first value' indicating that not only does the initiated value remain untouched,
// one can use the internally stored context reference rigorously without losing sight of local variables.     

fn2.checkval();
fn2.chain().chain().checkval();
// the fact that this set of tests returns 'second value'
// proves that they are really referencing separate instances

另一种使用此方法的方式是创建单例。我们的javascript函数往往不会被实例化超过一次。如果您知道永远不需要同一个函数的第二个实例,则有一种简写方式来创建它们。但是请注意:lint会抱怨它是奇怪的结构,并质疑您使用'new'关键字的用法:

fn=new function(val){
    var self=this;
    this.chain=function(){
        return self;
    };        

    this.checkval=function(){
        return val;
    };  
}    

fn.checkval();
fn.chain().chain().checkval();

优点: 使用该方法创建函数对象的好处很多。

  • 它使您的代码更易于阅读,因为它以一种让人视觉上更容易跟随的方式缩进了函数对象的方法。
  • 即使您稍后添加原型函数或成员函数到函数对象中,也只能在最初以这种方式定义的方法中访问本地定义的变量,它无法访问局部变量和您在该级别存储的任何功能或数据都保持安全并且无法从其他任何地方访问。
  • 它提供了一种简单而直接的方法来定义单例。
  • 它允许您存储对“this”的引用并无限期地维护该引用。

缺点: 使用此方法存在一些缺点。我不打算详尽说明 :)

  • 因为方法被定义为对象的成员而不是原型 - 通过成员定义可以实现继承但不能通过原型定义实现。 实际上,这是不正确的。可以通过对f.constructor.prototype进行操作来实现相同的原型继承。

这是一个不错的方法,但它还存在一个更微妙的问题,在某些情况下可能会出现。当您使用构造函数返回方法时,new运算符甚至不再返回原型链。也就是说,它不是被隐藏或覆盖了 - 它不存在了。您在原型链上拥有的任何成员 - 比如来自超类的成员 - 都消失了。 - dhimes
@dhimes - 实际上,你无法访问原型链的唯一原因是你不再有访问构造函数的权限。但我们可以通过<function>.constructor属性来访问它。测试一下就能证明:a=new function(){}; a.constructor.prototype.b=function(){console.log('in .b');}; a.b(); - Nemesarial

4

1
你可以创建对该对象的引用,也可以使用with (this)方法。当你在使用事件处理程序并且没有办法传入一个引用时,后者非常有用。
MyClass = function() {
    // More code here ...
}

MyClass.prototype.myfunc = function() {       
    // Create a reference
    var obj = this;
    this.element.click(function() {
        // "obj" refers to the original class instance            
        with (this){
            // "this" now also refers to the original class instance
        }
    });

}

4
由于存在歧义和其他问题,应避免使用with语句。 - Jimmy
当然,如果你可以避免使用它,那么尽管使用更简单的方法,但当其他方法都失败时,它仍然是有效和有用的。 - Ali C
-1:with 不会改变 this 的值。 - Bergi

1

既然你正在使用jQuery,值得注意的是this已经由jQuery本身维护:

$("li").each(function(j,o){
  $("span", o).each(function(x,y){
    alert(o + " " + y);
  });
});

在这个例子中,o代表li,而y代表子元素span。使用$.click(),你可以从event对象中获取作用域:
$("li").click(function(e){
  $("span", this).each(function(i,o){
    alert(e.target + " " + o);
  });
});

e.target 表示 li,而 o 表示子元素 span


0
另一种解决方案(也是我在jQuery中最喜欢的方法)是使用jQuery提供的'e.data'来传递'this'。然后你可以这样做:
this.element.bind('click', this, function(e) {
    e.data.myValue; //e.data now references the 'this' that you want
});

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