Passar o contexto "this" correto para o retorno de chamada setTimeout?

249

Como passo o contexto setTimeout? Eu quero ligar this.tip.destroy()se this.options.destroyOnHidedepois de 1000 ms. Como eu posso fazer isso?

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

Quando tento o acima, thisrefere-se à janela.

JamesBrownIsDead
fonte
4
O sinalizador duplicado é realmente válido? Esta pergunta foi realmente feita anteriormente.
Sui Dream
1
if (this.options.destroyOnHide) {setTimeout (function () {this.tip.destroy ()} .bind (this), 1000); }
Zibri

Respostas:

352

EDIT: Em resumo, em 2010, quando essa pergunta foi feita, a maneira mais comum de resolver esse problema era salvar uma referência ao contexto em que a setTimeoutchamada da função é feita, porque setTimeoutexecuta a função thisapontando para o objeto global:

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

Na especificação do ES5, lançada um ano antes dessa época, ele introduziu o bindmétodo , isso não foi sugerido na resposta original porque ainda não era amplamente suportado e você precisava de polyfills para usá-lo, mas agora está em toda parte:

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

A bindfunção cria uma nova função com o thisvalor pré-preenchido.

Agora, no JS moderno, esse é exatamente o problema que as funções de seta resolvem no ES6 :

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

As funções de seta não têm um thisvalor próprio; quando você acessa, está acessando o thisvalor do escopo lexical anexo.

O HTML5 também padronizou os temporizadores em 2011 e agora você pode passar argumentos para a função de retorno de chamada:

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

Veja também:

CMS
fonte
3
Funciona. Testei o conceito com um script jsbin
John K
1
Esse código envolve criar uma variável desnecessária (que tem escopo para toda a função); se você passasse corretamente thispara a função, teria resolvido esse problema nesse caso, para map (), forEach () etc., etc., usando menos código, menos ciclos de CPU e menos memória. *** Veja: resposta de Misha Reyzlin.
HoldOffHunger
222

Existem atalhos prontos (açúcar sintático) para o wrapper de função @CMS respondido. (Abaixo, assumindo que o contexto que você deseja é this.tip.)


ECMAScript 5 ( navegadores atuais , Node.js) e Prototype.js

Se você segmentar um navegador compatível com ECMA-262, 5ª edição (ECMAScript 5) ou Node.js , poderá usar Function.prototype.bind. Opcionalmente, você pode transmitir qualquer argumento de função para criar funções parciais .

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

Novamente, no seu caso, tente o seguinte:

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

A mesma funcionalidade também foi implementada no Prototype (outras bibliotecas?).

Function.prototype.bindpode ser implementado assim se você desejar compatibilidade com versões anteriores personalizadas (mas observe as notas).


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

Para o desenvolvimento de ponta (2015), você pode usar as funções de seta gorda , que fazem parte da especificação do ECMAScript 2015 (Harmony / ES6 / ES2015) ( exemplos ).

Uma expressão de função de seta (também conhecida como função de seta gorda ) possui uma sintaxe mais curta em comparação com as expressões de função e vincula lexicamente o thisvalor [...].

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

No seu caso, tente o seguinte:

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

jQuery

Se você já usa o jQuery 1.4+, existe uma função pronta para definir explicitamente o thiscontexto de uma função.

jQuery.proxy () : assume uma função e retorna uma nova que sempre terá um contexto específico.

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

No seu caso, tente o seguinte:

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

Underscore.js , lodash

Está disponível no Underscore.js, bem como no lodash, como _.bind(...)1 , 2

bind Liga uma função a um objeto, o que significa que sempre que a função é chamada, o valor dethisserá o objeto. Opcionalmente, vincule argumentos à função para preenchê-los previamente, também conhecido como aplicativo parcial.

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

No seu caso, tente o seguinte:

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

Joel Purra
fonte
Por que não usar como padrão func.bind(context...)? Perco alguma coisa?
Atei
É de bom desempenho criar constantemente uma nova função (que vincula) toda vez que você chama isso? Eu tenho um tempo limite de pesquisa que é redefinido após cada pressionamento de tecla e parece que eu deveria estar colocando esse método 'vinculado' em algum lugar para reutilização.
Triynko
@Triynko: Eu não veria vincular uma função como uma operação cara, mas se você chamar a mesma função vinculada várias vezes, também poderá manter uma referência: var boundFn = fn.bind(this); boundFn(); boundFn();por exemplo.
Joel Purra
30

Em navegadores que não sejam o Internet Explorer, você pode passar parâmetros para a função juntos após o atraso:

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

Então, você pode fazer isso:

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

Isso é melhor em termos de desempenho do que uma pesquisa de escopo (armazenamento thisem cache em uma variável fora da expressão de tempo limite / intervalo) e, em seguida, criando um fechamento (usando $.proxyou Function.prototype.bind).

O código para fazê-lo funcionar no IEs a partir 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
fonte
1
Ao criar uma classe usando a cadeia de protótipos e seus métodos são métodos de protótipo ... 'bind' é a única coisa que altera o que 'this' está dentro do método. Ao passar um parâmetro para o retorno de chamada, você não altera o que 'this' está na função; portanto, uma função protótipo não pode ser gravada usando 'this' dentro dela, como qualquer outro método de protótipo. Isso leva à inconsistência. A associação é a coisa mais próxima do que realmente queremos, e o fechamento pode ser armazenado em cache 'this' para obter um desempenho mais alto da pesquisa e não ter que criá-lo mais de uma vez.
Triynko
3

NOTA: Isso não funcionará no 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);
gomas
fonte
2

Se você estiver usando underscore, você pode usar bind.

Por exemplo

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