¿Pasar el contexto "this" correcto a la devolución de llamada de Timeout?

249

¿Cómo paso el contexto setTimeout? Quiero llamar this.tip.destroy()si this.options.destroyOnHidedespués de 1000 ms. ¿Cómo puedo hacer eso?

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

Cuando intento lo anterior, se thisrefiere a la ventana.

JamesBrownIsDead
fuente
44
¿El indicador duplicado es realmente válido? Esta pregunta fue hecha antes.
Sui Dream
1
if (this.options.destroyOnHide) {setTimeout (function () {this.tip.destroy ()} .bind (this), 1000); }
Zibri

Respuestas:

352

EDITAR: En resumen, en 2010, cuando se hizo esta pregunta, la forma más común de resolver este problema era guardar una referencia al contexto donde se realiza la setTimeoutllamada a la función, porque setTimeoutejecuta la función thisapuntando al objeto global:

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

En la especificación ES5, recién lanzada un año antes de ese momento, introdujo el bindmétodo , esto no se sugirió en la respuesta original porque aún no era ampliamente compatible y necesitabas polyfills para usarlo, pero ahora está en todas partes:

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

La bindfunción crea una nueva función con el thisvalor precargado.

Ahora en JS moderno, este es exactamente el problema que las funciones de flecha resuelven en ES6 :

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

Las funciones de flecha no tienen un thisvalor propio, cuando accede a él, está accediendo al thisvalor del ámbito léxico adjunto.

HTML5 también estandarizó los temporizadores en 2011, y ahora puede pasar argumentos a la función de devolución de llamada:

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

Ver también:

CMS
fuente
3
Funciona. Probé el concepto con un script jsbin
John K
1
Este código implica hacer una variable innecesaria (que tiene un alcance de función amplia); si pasó correctamente thisa la función, habría resuelto este problema para este caso, para map (), forEach (), etc., etc., utilizando menos código, menos ciclos de CPU y menos memoria. *** Ver: la respuesta de Misha Reyzlin.
HoldOffHunger
222

Hay atajos ya preparados (azúcar sintáctico) para el contenedor de funciones con el que respondió @CMS. (A continuación, suponiendo que el contexto que desea es this.tip).


ECMAScript 5 ( navegadores actuales , Node.js) y Prototype.js

Si apunta a un navegador compatible con ECMA-262, 5a edición (ECMAScript 5) o Node.js , puede usarlo Function.prototype.bind. Opcionalmente, puede pasar cualquier argumento de función para crear funciones parciales .

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

Nuevamente, en su caso, intente esto:

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

La misma funcionalidad también se ha implementado en Prototype (¿alguna otra biblioteca?).

Function.prototype.bindpuede implementarse de esta manera si desea compatibilidad con versiones anteriores personalizadas (pero tenga en cuenta las notas).


ECMAScript 2015 ( algunos navegadores , Node.js 5.0.0+)

Para el desarrollo de vanguardia (2015) puede utilizar las funciones de flecha gruesa , que forman parte de la especificación ( ejemplos ) ECMAScript 2015 (Harmony / ES6 / ES2015 ).

Una expresión de función de flecha (también conocida como función de flecha gruesa ) tiene una sintaxis más corta en comparación con expresiones de función y enlaza léxicamente el thisvalor [...].

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

En su caso, intente esto:

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

jQuery

Si ya está utilizando jQuery 1.4+, hay una función lista para configurar explícitamente el thiscontexto de una función.

jQuery.proxy () : toma una función y devuelve una nueva que siempre tendrá un contexto particular.

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

En su caso, intente esto:

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

Underscore.js , lodash

Está disponible en Underscore.js, así como lodash, como _.bind(...)1 , 2

enlazar Vincula una función a un objeto, lo que significa que siempre que se llame a la función, el valor dethisserá el objeto. Opcionalmente, vincule argumentos a la función para completarlos previamente, también conocida como aplicación parcial.

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

En su caso, intente esto:

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

Joel Purra
fuente
¿Por qué no por defecto func.bind(context...)? ¿Echo de menos algo?
aTei
¿Es rentable seguir creando constantemente una nueva función (lo que hace el enlace) cada vez que llama a esto? Tengo un tiempo de espera de búsqueda que se restablece después de cada pulsación de tecla, y parece que debería estar almacenando en caché este método 'enlazado' en algún lugar para su reutilización.
Triynko
@Triynko: No vería que vincular una función como una operación costosa, pero si llama a la misma función enlazada varias veces, también podría mantener una referencia: var boundFn = fn.bind(this); boundFn(); boundFn();por ejemplo.
Joel Purra
30

En navegadores que no sean Internet Explorer, puede pasar parámetros a la función juntos después del retraso:

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

Entonces, puedes hacer esto:

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

Esto es mejor en términos de rendimiento que una búsqueda de alcance (almacenamiento thisen caché en una variable fuera del tiempo de espera / expresión de intervalo), y luego crear un cierre (mediante el uso de $.proxyo Function.prototype.bind).

El código para hacerlo funcionar en 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
fuente
1
Al crear una clase utilizando la cadena de prototipo y sus métodos son métodos de prototipo ... 'vincular' es lo único que alterará lo que 'esto' contiene dentro del método. Al pasar un parámetro a la devolución de llamada, no se altera lo que es 'esto' en la función, por lo que dicha función prototipo no se podría escribir usando 'esto' dentro de ella como cualquier otro método prototipo. Eso lleva a la inconsistencia. Bind es lo más parecido a lo que realmente queremos, y el cierre podría almacenarse en caché en 'esto' para un mayor rendimiento de búsqueda y no tener que crearlo más de una vez.
Triynko
3

NOTA: Esto no funcionará en 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);
chicles
fuente
2

Si está usando underscore, puede usarbind .

P.ej

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