web-dev-qa-db-fra.com

Préservation d'une référence à "this" dans les fonctions de prototype JavaScript

Je commence tout juste à utiliser du JavaScript prototypique et je ne parviens pas à comprendre comment conserver une référence this à l'objet principal dans une fonction prototype lorsque la portée change. Permettez-moi d'illustrer ce que je veux dire (j'utilise jQuery ici):

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();

Je sais que je peux conserver une référence à l'objet principal en faisant ceci au début de myfunc:

var myThis = this;

puis en utilisant myThis.myValue pour accéder à la propriété de l'objet principal. Mais que se passe-t-il lorsque j'ai un tas de fonctions de prototypes sur MyClass? Dois-je enregistrer la référence à this au début de chacun? On dirait qu'il devrait y avoir un moyen plus propre. Et que dire d'une situation comme celle-ci:

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

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

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

new MyClass();

Dans ce cas, je ne peux pas créer de référence à l'objet principal avec var myThis = this; car même la valeur d'origine de this dans le contexte de doSomething est un objet jQuery et non un objet MyClass.

On m'a suggéré d'utiliser une variable globale pour conserver la référence à la variable this originale, mais cela me semble une très mauvaise idée. Je ne veux pas polluer l'espace de noms global, ce qui m'empêcherait d'instancier deux objets MyClass différents sans qu'ils interfèrent l'un avec l'autre.

Aucune suggestion? Existe-t-il un moyen propre de faire ce que je recherche? Ou est-ce que tout mon motif est défectueux?

88
Jimmy Cuadra

Pour préserver le contexte, la méthode bind est vraiment utile, elle fait maintenant partie de la récente publication ECMAScript 5th Edition Specification, la mise en oeuvre de cette fonction est simple (8 lignes seulement):

// 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))); 
    }; 
  };
}

Et vous pourriez l'utiliser, dans votre exemple, comme ceci:

MyClass.prototype.myfunc = function() {

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

Un autre exemple:

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);

Dans ce deuxième exemple, nous pouvons en observer davantage sur le comportement de bind.

Il génère essentiellement une nouvelle fonction, qui sera chargée d'appeler notre fonction, en préservant le contexte de la fonction (valeur this), défini comme le premier argument de bind.

Le reste des arguments est simplement passé à notre fonction.

Notez dans cet exemple que la fonction fx1, est invoquée sans aucun contexte object (obj.method()), comme un simple appel de fonction, dans ce type d'appel, le mot clé this à l'intérieur fera référence à l'objet Global, il avertira "test global".

Maintenant, le fx2 est la nouvelle fonction générée par la méthode bind. Elle appellera notre fonction en préservant le contexte et en passant correctement les arguments. Elle alertera "obj test 1, 2, 3, 4, 5" car nous l'avons invoquée en ajoutant le deux arguments en plus, il avait déjà binded les trois premiers.

64
CMS

Pour votre dernier exemple MyClass, vous pouvez procéder comme suit:

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

Dans la fonction transmise à each, this fait référence à un objet jQuery, comme vous le savez déjà. Si à l'intérieur de cette fonction, vous obtenez la fonction doSomething à partir de myThis, puis appelez la méthode apply sur cette fonction avec le tableau d'arguments (voir la fonction apply et la variable arguments ), alors this sera défini sur myThis in doSomething.

10
icktoofay

Je réalise que c'est un vieux fil, mais j'ai une solution beaucoup plus élégante et qui présente peu d'inconvénients, mis à part le fait que ce n'est généralement pas le cas, comme je l'ai remarqué. 

Considérer ce qui suit: 

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

Dans la fonction ci-dessus, nous avons défini une variable locale (contexte). Nous avons ensuite ajouté une fonction prototype (test) qui renvoie la variable locale. Comme vous l'avez probablement prévu, lorsque nous créons une instance de cette fonction, puis exécutons la méthode de test, elle ne renvoie pas la variable locale, car lorsque nous avons défini la fonction prototype comme membre de notre fonction principale, elle se trouvait en dehors de la portée. la variable locale est définie . Il s'agit d'un problème général lié à la création de fonctions, puis à l'ajout de prototypes - vous ne pouvez accéder à aucun élément créé dans l'étendue de la fonction principale.

Pour créer des méthodes qui entrent dans la portée de la variable locale, nous devons les définir directement en tant que membres de la fonction et supprimer la référence prototype:

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;

Vous craignez peut-être que, du fait que les méthodes ne soient pas créées de manière prototypique, différentes instances ne soient pas réellement séparées des données. Pour démontrer qu'ils le sont, considérons ceci: 

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

Une autre façon d'utiliser cette méthode consiste à créer des singletons. Le plus souvent, nos fonctions javascript ne sont pas instanciées plus d'une fois. Si vous savez que vous n'aurez jamais besoin d'une seconde instance de la même fonction, il existe un moyen simple de les créer. Soyez averti cependant: lint se plaindra que c'est une construction étrange, et remettez en question votre utilisation du mot clé 'new':

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

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

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

Pro's: .__ Les avantages de l'utilisation de cette méthode pour créer des objets fonction sont nombreux. 

  • Cela facilite la lecture de votre code, car il indente les méthodes d'un objet fonction de manière à le rendre visuellement plus facile à suivre. 
  • Il permet l'accès aux variables définies localementuniquement dans les méthodes définies à l'origine de cette manièremême si vous ajoutez ultérieurement des fonctions prototypiques ou même des fonctions membres à l'objet fonction, il ne peut pas accéder aux variables locales ni aux fonctionnalités. ou les données que vous stockez à ce niveau restent sécurisées et inaccessibles ailleurs.
  • Cela permet de définir des singletons de manière simple et directe.
  • Il vous permet de stocker une référence à 'this' et de la conserver indéfiniment.

Con's: Il y a quelques inconvénients à utiliser cette méthode. Je ne prétends pas être complet :)

  • Les méthodes étant définies comme des membres de l'objet et non des prototypes, l'héritage peut être obtenu à l'aide de la définition de membre, mais pas de définitions prototypiques. Ceci est en fait incorrect. Le même héritage prototype peut être obtenu en agissant sur f.constructor.prototype
7
Nemesarial

Vous pouvez définir la portée en utilisant les fonctions call () et apply ()

4
Gabriel McAdams

Puisque vous utilisez jQuery, notez que this est déjà géré par jQuery lui-même:

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

Dans cet exemple, o représente la li, alors que y représente l'enfant span. Et avec $.click(), vous pouvez obtenir la portée de l’objet event:

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

e.target représente la li et o représente l'enfant span.

1
Sampson

Vous pouvez créer une référence à cet objet ou utiliser la méthode with (this). Ce dernier est extrêmement utile lorsque vous utilisez des gestionnaires d’événements et vous n’avez aucun moyen de transmettre une référence.

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
        }
    });

}
1
Ali C

Une autre solution (et ma méthode préférée dans jQuery) consiste à utiliser 'e.data' fourni par jQuery pour transmettre 'this'. Ensuite, vous pouvez faire ceci:

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