Passer le contexte «this» correct pour le rappel setTimeout?

249

Comment passer le contexte setTimeout? Je veux appeler this.tip.destroy()si this.options.destroyOnHideaprès 1000 ms. Comment puis je faire ça?

if (this.options.destroyOnHide) {
     setTimeout(function() { this.tip.destroy() }, 1000);
} 

Lorsque j'essaie ce qui précède, se thisréfère à la fenêtre.

JamesBrownIsDead
la source
4
Le drapeau en double est-il vraiment valide? Cette question a en fait été posée plus tôt.
Sui Dream
1
if (this.options.destroyOnHide) {setTimeout (function () {this.tip.destroy ()} .bind (this), 1000); }
Zibri

Réponses:

352

EDIT: En résumé, en 2010, lorsque cette question a été posée, le moyen le plus courant de résoudre ce problème était d'enregistrer une référence au contexte dans lequel l' setTimeoutappel de fonction est effectué, car setTimeoutexécute la fonction en thispointant vers l'objet global:

var that = this;
if (this.options.destroyOnHide) {
     setTimeout(function(){ that.tip.destroy() }, 1000);
} 

Dans la spécification ES5, juste publiée un an avant cette date, elle a introduit la bindméthode , ce qui n'était pas suggéré dans la réponse originale car elle n'était pas encore largement prise en charge et vous aviez besoin de polyfills pour l'utiliser, mais maintenant elle est partout:

if (this.options.destroyOnHide) {
     setTimeout(function(){ this.tip.destroy() }.bind(this), 1000);
}

La bindfonction crée une nouvelle fonction avec la thisvaleur pré-remplie.

Maintenant, dans JS moderne, c'est exactement le problème que posent les fonctions fléchées dans ES6 :

if (this.options.destroyOnHide) {
     setTimeout(() => { this.tip.destroy() }, 1000);
}

Les fonctions fléchées n'ont pas de thisvaleur propre, lorsque vous y accédez, vous accédez à la thisvaleur de la portée lexicale englobante.

HTML5 a également normalisé les minuteries en 2011, et vous pouvez maintenant passer des arguments à la fonction de rappel:

if (this.options.destroyOnHide) {
     setTimeout(function(that){ that.tip.destroy() }, 1000, this);
}

Voir également:

CMS
la source
3
Ça marche. J'ai testé le concept avec un script jsbin: jsbin.com/etise/7/edit
John K
1
Ce code implique de créer une variable inutile (qui a une portée à l'échelle de la fonction); si vous aviez correctement transmis thisla fonction, vous auriez résolu ce problème dans ce cas, pour map (), forEach (), etc., etc., en utilisant moins de code, moins de cycles CPU et moins de mémoire. *** Voir: la réponse de Misha Reyzlin.
HoldOffHunger
222

Il existe des raccourcis prêts à l'emploi (sucre syntaxique) vers l'encapsuleur de fonction auquel @CMS a répondu. (Ci-dessous, en supposant que le contexte que vous souhaitez est this.tip.)


ECMAScript 5 ( navigateurs actuels , Node.js) et Prototype.js

Si vous ciblez un navigateur compatible avec ECMA-262, 5e édition (ECMAScript 5) ou Node.js , vous pouvez utiliser Function.prototype.bind. Vous pouvez éventuellement passer n'importe quel argument de fonction pour créer des fonctions partielles .

fun.bind(thisArg[, arg1[, arg2[, ...]]])

Encore une fois, dans votre cas, essayez ceci:

if (this.options.destroyOnHide) {
    setTimeout(this.tip.destroy.bind(this.tip), 1000);
}

La même fonctionnalité a également été implémentée dans Prototype (toutes autres bibliothèques?).

Function.prototype.bindpeut être implémenté comme ceci si vous voulez une compatibilité descendante personnalisée (mais veuillez respecter les notes).


ECMAScript 2015 ( certains navigateurs , Node.js 5.0.0+)

Pour un développement de pointe (2015), vous pouvez utiliser les fonctions de flèche grasse , qui font partie de la spécification ECMAScript 2015 (Harmony / ES6 / ES2015) ( exemples ).

Une expression de fonction de flèche (également connue sous le nom de fonction de flèche de graisse ) a une syntaxe plus courte par rapport aux expressions de fonction et lie lexicalement la thisvaleur [...].

(param1, param2, ...rest) => { statements }

Dans votre cas, essayez ceci:

if (this.options.destroyOnHide) {
    setTimeout(() => { this.tip.destroy(); }, 1000);
}

jQuery

Si vous utilisez déjà jQuery 1.4+, il existe une fonction prête à l'emploi pour définir explicitement le thiscontexte d'une fonction.

jQuery.proxy () : prend une fonction et retourne une nouvelle qui aura toujours un contexte particulier.

$.proxy(function, context[, additionalArguments])

Dans votre cas, essayez ceci:

if (this.options.destroyOnHide) {
    setTimeout($.proxy(this.tip.destroy, this.tip), 1000);
}

Underscore.js , lodash

Il est disponible dans Underscore.js, ainsi que lodash, comme _.bind(...)1 , 2

bind Lier une fonction à un objet, ce qui signifie que chaque fois que la fonction est appelée, la valeur dethissera l'objet. Vous pouvez éventuellement lier des arguments à la fonction pour les pré-remplir, également appelés application partielle.

_.bind(function, object, [*arguments])

Dans votre cas, essayez ceci:

if (this.options.destroyOnHide) {
    setTimeout(_.bind(this.tip.destroy, this.tip), 1000);
}

Joel Purra
la source
Pourquoi pas par défaut func.bind(context...)? Dois-je manquer quelque chose?
aTei
Est-il efficace de constamment créer une nouvelle fonction (qui se lie) à chaque fois que vous appelez cela? J'ai un délai de recherche qui se réinitialise après chaque pression de touche, et il semble que je devrais mettre en cache cette méthode «liée» quelque part pour la réutiliser.
Triynko
@Triynko: Je ne verrais pas la liaison d'une fonction comme une opération coûteuse, mais si vous appelez la même fonction liée plusieurs fois, vous pourriez aussi bien garder une référence: var boundFn = fn.bind(this); boundFn(); boundFn();par exemple.
Joel Purra
30

Dans les navigateurs autres qu'Internet Explorer, vous pouvez transmettre des paramètres à la fonction ensemble après le délai:

var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]);

Donc, vous pouvez le faire:

var timeoutID = window.setTimeout(function (self) {
  console.log(self); 
}, 500, this);

C'est mieux en termes de performances qu'une recherche de portée (mise thisen cache dans une variable en dehors de l'expression de délai d'expiration / intervalle), puis de créer une fermeture (en utilisant $.proxyou Function.prototype.bind).

Le code pour le faire fonctionner dans les IE de Webreflection :

/*@cc_on
(function (modifierFn) {
  // you have to invoke it as `window`'s property so, `window.setTimeout`
  window.setTimeout = modifierFn(window.setTimeout);
  window.setInterval = modifierFn(window.setInterval);
})(function (originalTimerFn) {
    return function (callback, timeout){
      var args = [].slice.call(arguments, 2);
      return originalTimerFn(function () { 
        callback.apply(this, args) 
      }, timeout);
    }
});
@*/
Misha Reyzlin
la source
1
Lorsque vous créez une classe à l'aide de la chaîne de prototypes et que vos méthodes sont des méthodes prototypes ... 'bind' est la seule chose qui modifiera ce que 'this' est dans la méthode. En passant un paramètre à la fonction de rappel, vous ne modifiez pas ce que «ceci» est dans la fonction, donc une telle fonction prototype ne peut pas être écrite en utilisant «ceci» à l'intérieur comme n'importe quelle autre méthode prototype. Cela conduit à une incohérence. La liaison est la chose la plus proche de ce que nous voulons réellement, et la fermeture pourrait être mise en cache dans «ceci» pour des performances de recherche plus élevées et ne pas avoir à la créer plus d'une fois.
Triynko
3

REMARQUE: cela ne fonctionnera pas dans IE

var ob = {
    p: "ob.p"
}

var p = "window.p";

setTimeout(function(){
    console.log(this.p); // will print "window.p"
},1000); 

setTimeout(function(){
    console.log(this.p); // will print "ob.p"
}.bind(ob),1000);
gumkins
la source
2

Si vous utilisez underscore, vous pouvez utiliser bind.

Par exemple

if (this.options.destroyOnHide) {
     setTimeout(_.bind(this.tip.destroy, this), 1000);
}
Sam
la source