Como acessar o correto `this` dentro de um callback?

Eu tenho uma function de construtor que registra um manipulador de events:

function MyConstructor(data, transport) { this.data = data; transport.on('data', function () { alert(this.data); }); } // Mock transport object var transport = { on: function(event, callback) { setTimeout(callback, 1000); } }; // called as var obj = new MyConstructor('foo', transport); 

No entanto, não consigo acessar a propriedade data do object criado dentro do retorno de chamada. Parece que this não se refere ao object que foi criado, mas a outro.

Eu também tentei usar um método de object em vez de uma function anônima:

 function MyConstructor(data, transport) { this.data = data; transport.on('data', this.alert); } MyConstructor.prototype.alert = function() { alert(this.name); }; 

mas exibe os mesmos problemas.

Como posso acessar o object correto?

O que você deve saber sobre this

this (também conhecido como “o contexto”) é uma palavra-chave especial dentro de cada function e seu valor depende apenas de como a function foi chamada, não como / quando / onde foi definida. Não é afetado por escopos lexicais como outras variables ​​(exceto para funções de seta, veja abaixo). aqui estão alguns exemplos:

 function foo() { console.log(this); } // normal function call foo(); // `this` will refer to `window` // as object method var obj = {bar: foo}; obj.bar(); // `this` will refer to `obj` // as constructor function new foo(); // `this` will refer to an object that inherits from `foo.prototype` 

Para saber mais sobre this , dê uma olhada na documentação do MDN .


Como se referir ao correto

Não use this

Você realmente não quer acessar this em particular, mas o object a que se refere . É por isso que uma solução fácil é simplesmente criar uma nova variável que também se refira a esse object. A variável pode ter qualquer nome, mas os mais comuns são self e that .

 function MyConstructor(data, transport) { this.data = data; var self = this; transport.on('data', function() { alert(self.data); }); } 

Como self é uma variável normal, ela obedece às regras do escopo léxico e é acessível dentro do callback. Isso também tem a vantagem de poder acessar o valor do próprio retorno de chamada.

Defina explicitamente this rechamada – parte 1

Pode parecer que você não tem controle sobre o valor this porque seu valor é definido automaticamente, mas isso não é realmente o caso.

Cada function possui o método .bind [docs] , que retorna uma nova function com this associado a um valor. A function tem exatamente o mesmo comportamento que você chamou .bind on, só que this foi definido por você. Não importa como ou quando essa function é chamada, this sempre se referirá ao valor passado.

 function MyConstructor(data, transport) { this.data = data; var boundFunction = (function() { // parenthesis are not necessary alert(this.data); // but might improve readability }).bind(this); // < - here we are calling `.bind()` transport.on('data', boundFunction); } 

Neste caso, estamos vinculando o retorno de chamada com o valor do MyConstructor .

Nota: Ao vincular o contexto para jQuery, use jQuery.proxy [docs] . A razão para fazer isso é para que você não precise armazenar a referência para a function ao desvincular um retorno de chamada de evento. O jQuery manipula isso internamente.

ECMAScript 6: Use as funções de seta

O ECMAScript 6 introduz funções de seta , que podem ser consideradas como funções lambda. Eles não têm a sua própria binding. Em vez disso, this é procurado no escopo como uma variável normal. Isso significa que você não precisa chamar .bind . Esse não é o único comportamento especial que eles têm, por favor consulte a documentação do MDN para mais informações.

 function MyConstructor(data, transport) { this.data = data; transport.on('data', () => alert(this.data)); } 

Defina this do retorno de chamada - parte 2

Algumas funções / methods que aceitam retornos de chamada também aceitam um valor ao qual o retorno de chamada deve se referir. Isso é basicamente o mesmo que vinculá-lo, mas a function / método faz isso por você. Array#map [docs] é esse método. Sua assinatura é:

 array.map(callback[, thisArg]) 

O primeiro argumento é o retorno de chamada e o segundo argumento é o valor ao qual this deve se referir. Aqui está um exemplo artificial:

 var arr = [1, 2, 3]; var obj = {multiplier: 42}; var new_arr = arr.map(function(v) { return v * this.multiplier; }, obj); // < - here we are passing `obj` as second argument 

Nota: Se você pode ou não passar um valor para this é geralmente mencionado na documentação dessa function / método. Por exemplo, o método $.ajax [docs] do jQuery descreve uma opção chamada context :

Esse object será feito no contexto de todos os retornos de chamada relacionados ao Ajax.


Problema comum: usando methods de object como callbacks / manipuladores de events

Outra manifestação comum desse problema é quando um método de object é usado como manipulador de retorno de chamada / evento. Funções são cidadãos de primeira class em JavaScript e o termo "método" é apenas um termo coloquial para uma function que é um valor de uma propriedade de object. Mas essa function não possui um link específico para seu object "contendo".

Considere o seguinte exemplo:

 function Foo() { this.data = 42, document.body.onclick = this.method; } Foo.prototype.method = function() { console.log(this.data); }; 

A function this.method é atribuída como manipulador de events click, mas se o document.body for clicado, o valor registrado será undefined , porque dentro do manipulador de events, this se refere ao document.body , não à instância do Foo .
Como já mencionado no início, o que this se refere depende de como a function é chamada , e não como ela é definida .
Se o código for semelhante ao seguinte, pode ser mais óbvio que a function não tenha uma referência implícita ao object:

 function method() { console.log(this.data); } function Foo() { this.data = 42, document.body.onclick = this.method; } Foo.prototype.method = method; 

A solução é a mesma mencionada acima: Se disponível, use .bind para vincular explicitamente this a um valor específico

 document.body.onclick = this.method.bind(this); 

ou chame explicitamente a function como um "método" do object, usando uma function anônima como manipulador de retorno de chamada / evento e atribua o object ( this ) a outra variável:

 var self = this; document.body.onclick = function() { self.method(); }; 

ou use uma function de seta:

 document.body.onclick = () => this.method(); 

Aqui estão várias maneiras de acessar o contexto pai dentro do contexto filho –

  1. Você pode usar a function bind () .
  2. Armazene a referência ao contexto / isto dentro de outra variável (veja o exemplo abaixo).
  3. Use as funções de seta ES6.
  4. Altere o código / design de function / arquitetura – para isso você deve ter o comando sobre os padrões de projeto em javascript.

1. Use a function bind()

 function MyConstructor(data, transport) { this.data = data; transport.on('data', ( function () { alert(this.data); }).bind(this) ); } // Mock transport object var transport = { on: function(event, callback) { setTimeout(callback, 1000); } }; // called as var obj = new MyConstructor('foo', transport); 

Se você estiver usando underscore.jshttp://underscorejs.org/#bind

 transport.on('data', _.bind(function () { alert(this.data); }, this)); 

2 Armazene a referência ao contexto / isto dentro de outra variável

 function MyConstructor(data, transport) { var self = this; this.data = data; transport.on('data', function() { alert(self.data); }); } 

3 function de seta

 function MyConstructor(data, transport) { this.data = data; transport.on('data', () => { alert(this.data); }); } 

Está tudo na syntax “mágica” de chamar um método:

 object.property(); 

Quando você obtém a propriedade do object e a chama de uma só vez, o object será o contexto para o método. Se você chamar o mesmo método, mas em etapas separadas, o contexto será o escopo global (janela):

 var f = object.property; f(); 

Quando você obtém a referência de um método, ele não é mais anexado ao object, é apenas uma referência a uma function simples. O mesmo acontece quando você obtém a referência para usar como um retorno de chamada:

 this.saveNextLevelData(this.setAll); 

É aí que você ligaria o contexto à function:

 this.saveNextLevelData(this.setAll.bind(this)); 

Se você estiver usando o jQuery, deverá usar o método $.proxy , já que a bind não é suportada em todos os navegadores:

 this.saveNextLevelData($.proxy(this.setAll, this)); 

O problema com “contexto”

O termo “contexto” é usado às vezes para se referir ao object referenciado por isso . Seu uso é inadequado porque não se encheckbox semanticamente ou tecnicamente com o ECMAScript .

“Contexto” significa as circunstâncias que envolvem algo que adiciona significado, ou alguma informação anterior e posterior que dê significado extra. O termo “contexto” é usado no ECMAScript para referir-se ao contexto de execução , que é todos os parâmetros, escopo e isto dentro do escopo de algum código de execução.

Isso é mostrado na seção 10.4.2 da ECMA-262 :

Defina o ThisBinding para o mesmo valor que o ThisBinding do contexto de execução de chamada

que indica claramente que isso faz parte de um contexto de execução.

Um contexto de execução fornece a informação circundante que adiciona significado ao código que está sendo executado. Inclui muito mais informações que apenas o thisBinding .

Portanto, o valor disso não é “contexto”, é apenas uma parte de um contexto de execução. É essencialmente uma variável local que pode ser definida pela chamada para qualquer object e no modo estrito, para qualquer valor.

Primeiro, você precisa ter um entendimento claro do scope e do comportamento this palavra this chave no contexto do scope .

this e scope :


 there are two types of scope in javascript. They are : 1) Global Scope 2) Function Scope 

em suma, escopo global refere-se ao object de janela.Variables declarados em um escopo global são acessíveis de qualquer lugar.Por outro lado, escopo de function reside dentro de uma function.variável declarada dentro de uma function não pode ser acessado do mundo externo normalmente. this palavra this chave no escopo global refere-se ao object da janela. this function interna também se refere ao object da janela. Portanto, this sempre se referirá à janela até encontrarmos uma maneira de manipulá- this para indicar um contexto de nossa própria escolha.

 -------------------------------------------------------------------------------- - - - Global Scope - - ( globally "this" refers to window object) - - - - function outer_function(callback){ - - - - // outer function scope - - // inside outer function"this" keyword refers to window object - - - callback() // "this" inside callback also refers window object - - } - - - - function callback_function(){ - - - - // function to be passed as callback - - - - // here "THIS" refers to window object also - - - - } - - - - outer_function(callback_function) - - // invoke with callback - -------------------------------------------------------------------------------- 

Diferentes maneiras de manipular this funções internas de retorno de chamada:

Aqui eu tenho uma function construtora chamada Person. Tem uma propriedade chamada name e quatro methods chamados sayNameVersion1 , sayNameVersion2 , sayNameVersion3 , sayNameVersion4 . Todos os quatro deles tem uma tarefa específica.Aceitar um retorno de chamada e invocá-lo.O retorno de chamada tem uma tarefa específica que é registrar a propriedade name de uma instância da function de construtor Person.

 function Person(name){ this.name = name this.sayNameVersion1 = function(callback){ callback.bind(this)() } this.sayNameVersion2 = function(callback){ callback() } this.sayNameVersion3 = function(callback){ callback.call(this) } this.sayNameVersion4 = function(callback){ callback.apply(this) } } function niceCallback(){ // function to be used as callback var parentObject = this console.log(parentObject) } 

Agora vamos criar uma instância a partir do construtor person e invocar diferentes versões do sayNameVersionX (X refere-se ao 1,2,3,4) com niceCallback para ver quantas maneiras podemos manipular o dentro this callback para se referir à instância person .

 var p1 = new Person('zami') // create an instance of Person constructor 

ligar :

O que a binding faz é criar uma nova function com a palavra this chave this configurada para o valor fornecido.

sayNameVersion1 e sayNameVersion2 usam bind para manipular this da function de retorno de chamada.

 this.sayNameVersion1 = function(callback){ callback.bind(this)() } this.sayNameVersion2 = function(callback){ callback() } 

primeiro um bind this com callback dentro do próprio método.E para o segundo callback é passado com o object ligado a ele.

 p1.sayNameVersion1(niceCallback) // pass simply the callback and bind happens inside the sayNameVersion1 method p1.sayNameVersion2(niceCallback.bind(p1)) // uses bind before passing callback 

ligar :

O first argument do método de call é usado como this dentro da function que é invocada com a call anexada a ele.

sayNameVersion3 usa a call para manipular this para se referir ao object pessoa que criamos, em vez do object da janela.

 this.sayNameVersion3 = function(callback){ callback.call(this) } 

e é chamado como o seguinte:

 p1.sayNameVersion3(niceCallback) 

Aplique :

Semelhante à call , o primeiro argumento de apply refere-se ao object que será indicado por this palavra this chave.

sayNameVersion4 usa apply para manipular this para se referir ao object pessoa

 this.sayNameVersion4 = function(callback){ callback.apply(this) } 

e é chamado como o seguinte.Simplesmente o retorno de chamada é passado,

 p1.sayNameVersion4(niceCallback) 

Nós não podemos vincular isso a setTimeout() , como sempre executa com object global (Window) , se você quiser acessar this contexto na function de callback, então usando bind() para a function callback que podemos obter como:

 setTimeout(function(){ this.methodName(); }.bind(this), 2000); 

Outra abordagem, que é a maneira padrão desde DOM2 para vincular this no ouvinte de evento, que permite sempre remover o ouvinte (entre outros benefícios), é o método handleEvent(evt) da interface EventListener :

 var obj = { handleEvent(e) { // always true console.log(this === obj); } }; document.body.addEventListener('click', obj); 

Informações detalhadas sobre o uso do handleEvent podem ser encontradas aqui: https://medium.com/@WebReflection/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38