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

IT技术 javascript oop scope this prototype-programming
2021-02-01 11:19:17

我刚刚开始使用原型 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;因为即使this是上下文中的原始值doSomething也是jQuery对象而不是MyClass对象。

有人建议我使用全局变量来保存对原始 的引用this,但这对我来说似乎是一个非常糟糕的主意。我不想污染全局命名空间,这似乎会阻止我在MyClass不相互干扰的情况下实例化两个不同的对象。

有什么建议?有没有一种干净的方法来做我所追求的?还是我的整个设计模式有缺陷?

6个回答

为了保留上下文,这个bind方法真的很有用,它现在是最近发布的ECMAScript 5th Edition Specification 的一部分,这个函数的实现很简单(只有 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

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

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

注意在这个例子中,函数fx1, 是在没有任何对象上下文( obj.method()) 的情况下调用的,就像一个简单的函数调用一样,在这种类型的调用中,this里面关键字将引用全局对象,它会警告“全局测试”。

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

我强烈建议坚持使用 name Function.prototype.bind它现在是语言的标准化部分;它不会消失。
2021-03-18 11:19:17
很高兴了解浏览器错误。仅供参考,jQuery 1.4 现在包括jQuery.proxy,其功能相似(虽然不相同)。像这样使用$.proxy(obj.fx, obj)$.proxy(obj, "fx")
2021-03-20 11:19:17
这是 JS 的一大缺点。在大型项目中,它会使代码看起来一团糟。闭包的想法是给 JS 带来的最糟糕的想法之一。似乎根本无法将对象原型的上下文绑定到实际对象。
2021-03-20 11:19:17
我真的很喜欢这个功能,但在 jQuery 环境中,鉴于现有的 jQuery.bind 方法(即使没有实际的命名冲突),我倾向于将其命名为其他名称。
2021-03-24 11:19:17
@bobnice:完全同意,原生实现将很快在主要 JavaScript 引擎中可用... bugs.webkit.org/show_bug.cgi?id=26382 bugzilla.mozilla.org/show_bug.cgi?id=429507
2021-04-04 11:19:17

对于你的最后一个MyClass例子,你可以这样做:

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

如您所知,在传递给 的函数中eachthis指的是一个 jQuery 对象。如果在该函数中您doSomething从 中获取函数myThis,然后使用参数数组调用该函数上的 apply 方法(请参阅apply函数arguments变量),this则将设置为myThisin doSomething

这是行不通的,当您到达 this.doSomething 时,this它已经被 jQuery 替换为其中一个元素。
2021-03-21 11:19:17
是的,当我最初发布它时有两个问题。我编辑了它,现在它应该可以工作了。(对于那个很抱歉...)
2021-04-05 11:19:17

我意识到这是一个旧线程,但我有一个更优雅的解决方案,除了我注意到的通常不会完成的事实之外,几乎没有缺点。

考虑以下:

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

在上面的函数中,我们定义了一个局部变量(上下文)。然后我们添加了一个返回局部变量的原型函数(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操作符甚至不再返回原型链。也就是说,这不是隐藏或覆盖的问题——它不存在。你在原型链上拥有的任何成员——比如来自超类的成员,都消失了。
2021-03-20 11:19:17
@dhimes - 实际上,您无法访问原型链的唯一原因是您无法再访问构造函数。除了我们确实可以通过<function>.constructor财产访问它对此进行测试以证明:a=new function(){}; a.constructor.prototype.b=function(){console.log('in .b');}; a.b();
2021-04-01 11:19:17

您可以使用call() 和 apply() 函数设置范围

由于您使用的是 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