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

Preserving a reference to “this” in JavaScript prototype functions

本问题已经有最佳答案,请猛点这里访问。

我刚开始使用原型Javascript,我很难弄清楚当作用域发生变化时,如何从原型函数内部保留对主对象的this引用。让我举例说明我的意思(我在这里使用jquery):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的开头这样做来保留对主要对象的引用:

1
var myThis = this;

然后使用myThis.myValue访问主对象的属性。但是当我在MyClass上有一大堆原型函数时会发生什么呢?我是否必须在每个开始时保存对this的引用?似乎应该有一个更干净的方法。像这样的情况怎么样:

1
2
3
4
5
6
7
8
9
10
11
12
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对象,而不会相互干扰。

有什么建议吗?有没有一个干净的方法来做我想要的?或者我的整个设计模式有缺陷?


为了保存上下文,bind方法非常有用,它现在是最新发布的ECMAScript第5版规范的一部分,此函数的实现很简单(只有8行长):

1
2
3
4
5
6
7
8
9
10
11
// 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)));
    };
  };
}

您可以使用它,在您的示例中如下所示:

1
2
3
4
5
6
MyClass.prototype.myfunc = function() {

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

另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
  test: 'obj test',
  fx: function() {
    alert(this.test + '
'
+ 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关键字将引用全局对象,它将警告"全局测试"。

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


对于上一个MyClass示例,您可以这样做:

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

在传递给each的函数中,this指的是jquery对象,您已经知道了。如果在该函数中从myThis中得到doSomething函数,然后用参数数组调用该函数的apply方法(见apply函数和arguments变量),那么this将在doSomething中设置为myThis


我意识到这是一条旧线,但我有一个更优雅的解决方案,除了我注意到的一般不做之外,它几乎没有缺点。

考虑以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
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)。正如您可能预测的那样,当我们创建这个函数的实例,然后执行测试方法时,它不会返回局部变量,因为当我们将原型函数定义为主函数的成员时,它不在定义局部变量的范围之内。这是创建函数然后向其添加原型的一般问题——您不能访问在主函数范围内创建的任何内容。

要创建在局部变量范围内的方法,我们需要直接将它们定义为函数的成员,并去掉原型引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

您可能会担心,因为这些方法不是以原型方式创建的,所以不同的实例可能不会真正被数据分离。要证明它们是,请考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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"的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn=new function(val){
    var self=this;
    this.chain=function(){
        return self;
    };        

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

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

Pro的:使用这种方法创建函数对象的好处是丰富的。

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

Con:使用这种方法有一些缺点。我不会假装很全面。)

  • 因为方法被定义为对象的成员而不是原型-继承可以通过使用成员定义而不是原型定义来实现。这实际上是不正确的。同样的原型继承可以通过作用于f.constructor.prototype来实现。


可以使用call()和apply()函数设置作用域。


您可以创建对此对象的引用,也可以使用with (this)方法。当您使用事件处理程序并且无法传递引用时,后者非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
        }
    });

}


由于您使用的是jquery,因此值得注意的是,jquery本身已经维护了this

1
2
3
4
5
$("li").each(function(j,o){
  $("span", o).each(function(x,y){
    alert(o +"" + y);
  });
});

在本例中,o表示li,而y表示span的子级。使用$.click()可以从event对象中得到作用域:

1
2
3
4
5
$("li").click(function(e){
  $("span", this).each(function(i,o){
    alert(e.target +"" + o);
  });
});

其中,e.target代表lio代表span子代。


另一个解决方案(也是我在jquery中最喜欢的方法)是使用jquery提供的"e.data"传递"this"。然后你可以这样做:

1
2
3
this.element.bind('click', this, function(e) {
    e.data.myValue; //e.data now references the 'this' that you want
});