JavaScript: o que isso significa?

Descobrir o valor de this pode ser complicado no JavaScript. Confira como fazer isso:

Jake Archibald
Jake Archibald

O this do JavaScript é motivo de muitas piadas, e isso acontece porque ele é muito complicado. No entanto, já vi desenvolvedores fazerem coisas muito mais complicadas e específicas do domínio para evitar lidar com esse this. Se você não tiver certeza sobre this, talvez isso ajude. Este é meu guia de this.

Vou começar com a situação mais específica e terminar com a menos específica. Este artigo é como um grande if (…) … else if () … else if (…) …, então você pode ir direto para a primeira seção que corresponde ao código que você está vendo.

  1. Se a função for definida como uma função de seta
  2. Caso contrário, se a função/classe for chamada com new
  3. Caso contrário, se a função tiver um valor this "limitado"
  4. Caso contrário, se this for definido no call-time
  5. Caso contrário, se a função for chamada por um objeto pai (parent.func())
  6. Caso contrário, se a função ou o escopo pai estiver no modo estrito
  7. Caso contrário

Se a função for definida como uma função de seta:

const arrowFunction = () => {
  console
.log(this);
};

Nesse caso, o valor de this é sempre igual a this no escopo pai:

const outerThis = this;

const arrowFunction = () => {
 
// Always logs `true`:
  console
.log(this === outerThis);
};

As funções de seta são ótimas porque o valor interno de this não pode ser alterado. Ele sempre é o mesmo que o this externo.

Outros exemplos

Com as funções de seta, o valor de this não pode ser alterado com bind:

// Logs `true` - bound `this` value is ignored:
arrowFunction
.bind({foo: 'bar'})();

Com as funções de seta, o valor de this não pode ser alterado com call ou apply:

// Logs `true` - called `this` value is ignored:
arrowFunction
.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction
.apply({foo: 'bar'});

Com as funções de seta, o valor de this não pode ser alterado chamando a função como membro de outro objeto:

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj
.arrowFunction();

Com as funções de seta, o valor de this não pode ser alterado chamando a função como um construtor:

// TypeError: arrowFunction is not a constructor
new arrowFunction();

Métodos de instância "Bound"

Com métodos de instância, se você quiser garantir que this sempre se refira à instância da classe, a melhor maneira é usar funções de seta e campos de classe:

class Whatever {
  someMethod
= () => {
   
// Always the instance of Whatever:
    console
.log(this);
 
};
}

Esse padrão é muito útil ao usar métodos de instância como listeners de eventos em componentes (como componentes do React ou da Web).

O exemplo acima pode parecer que está violando a regra "this será o mesmo que this no escopo pai", mas começa a fazer sentido se você pensar nos campos de classe como açúcar sintático para definir coisas no construtor:

class Whatever {
  someMethod
= (() => {
   
const outerThis = this;
   
return () => {
     
// Always logs `true`:
      console
.log(this === outerThis);
   
};
 
})();
}

// …is roughly equivalent to:

class Whatever {
  constructor
() {
   
const outerThis = this;
   
this.someMethod = () => {
     
// Always logs `true`:
      console
.log(this === outerThis);
   
};
 
}
}

Padrões alternativos envolvem vincular uma função existente no construtor ou atribuir a função no construtor. Se você não puder usar campos de classe por algum motivo, atribuir funções no construtor é uma alternativa razoável:

class Whatever {
  constructor
() {
   
this.someMethod = () => {
     
// …
   
};
 
}
}

Caso contrário, se a função/classe for chamada com new:

new Whatever();

O código acima vai chamar Whatever (ou a função de construtor dela, se for uma classe) com this definido como o resultado de Object.create(Whatever.prototype).

class MyClass {
  constructor
() {
    console
.log(
     
this.constructor === Object.create(MyClass.prototype).constructor,
   
);
 
}
}

// Logs `true`:
new MyClass();

O mesmo vale para construtores mais antigos:

function MyClass() {
  console
.log(
   
this.constructor === Object.create(MyClass.prototype).constructor,
 
);
}

// Logs `true`:
new MyClass();

Outros exemplos

Quando chamado com new, o valor de this não pode ser alterado com bind:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

Quando chamado com new, o valor de this não pode ser alterado chamando a função como membro de outro objeto:

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

Caso contrário, se a função tiver um valor this "bound":

function someFunction() {
 
return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

Sempre que boundFunction for chamado, o valor this dele será o objeto transmitido para bind (boundObject).

// Logs `false`:
console
.log(someFunction() === boundObject);
// Logs `true`:
console
.log(boundFunction() === boundObject);

Outros exemplos

Ao chamar uma função vinculada, o valor de this não pode ser alterado com call ou apply:

// Logs `true` - called `this` value is ignored:
console
.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console
.log(boundFunction.apply({foo: 'bar'}) === boundObject);

Ao chamar uma função vinculada, o valor de this não pode ser alterado chamando a função como um membro de outro objeto:

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console
.log(obj.boundFunction() === boundObject);

Caso contrário, se this for definido no momento da chamada:

function someFunction() {
 
return this;
}

const someObject = {hello: 'world'};

// Logs `true`:
console
.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console
.log(someFunction.apply(someObject) === someObject);

O valor de this é o objeto transmitido para call/apply.

Infelizmente, this é definido como algum outro valor por elementos como listeners de eventos do DOM, e o uso dele pode resultar em um código difícil de entender:

O que não fazer
element.addEventListener('click', function (event) {
 
// Logs `element`, since the DOM spec sets `this` to
 
// the element the handler is attached to.
  console
.log(this);
});

Evito usar this em casos como o acima e, em vez disso:

O que fazer
element.addEventListener('click', (event) => {
 
// Ideally, grab it from a parent scope:
  console
.log(element);
 
// But if you can't do that, get it from the event object:
  console
.log(event.currentTarget);
});

Caso contrário, se a função for chamada por um objeto pai (parent.func()):

const obj = {
  someMethod
() {
   
return this;
 
},
};

// Logs `true`:
console
.log(obj.someMethod() === obj);

Nesse caso, a função é chamada como membro de obj, então this será obj. Isso acontece no momento da chamada. Portanto, o link será quebrado se a função for chamada sem o objeto pai ou com um objeto pai diferente:

const {someMethod} = obj;
// Logs `false`:
console
.log(someMethod() === obj);

const anotherObj = {someMethod};
// Logs `false`:
console
.log(anotherObj.someMethod() === obj);
// Logs `true`:
console
.log(anotherObj.someMethod() === anotherObj);

someMethod() === obj é falso porque someMethod não é chamado como membro de obj. Talvez você tenha encontrado esse problema ao tentar algo como este:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

Isso falha porque a implementação de querySelector verifica o próprio valor this e espera que ele seja um nó DOM de tipos, e o exemplo acima interrompe essa conexão. Para fazer isso corretamente:

const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);

Divertido fato: nem todas as APIs usam this internamente. Métodos de console como console.log foram alterados para evitar referências a this. Portanto, log não precisa ser vinculado a console.

Caso contrário, se a função ou o escopo pai estiver no modo restrito:

function someFunction() {
 
'use strict';
 
return this;
}

// Logs `true`:
console
.log(someFunction() === undefined);

Nesse caso, o valor de this é indefinido. 'use strict' não é necessário na função se o escopo pai estiver no modo rígido (e todos os módulos estiverem no modo rígido).

Se esse não for seu caso, faça o seguinte:

function someFunction() {
 
return this;
}

// Logs `true`:
console
.log(someFunction() === globalThis);

Nesse caso, o valor de this é o mesmo que globalThis.

Ufa.

Pronto. Isso é tudo que sei sobre this. Dúvidas? Perdeu alguma coisa? Fique à vontade para me enviar um tweet.

Agradecemos a Mathias Bynens, Ingvar Stepanyan e Thomas Steiner pela revisão.