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 é bem 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 if (…) … else if () … else if (…) … grande, 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 estiver definida como uma arrow function
  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 usando 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 estiver definida como uma seta de função:

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âncias "vinculadas"

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 componentes da Web).

Pode parecer que isso está quebrando 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 uma facilidade sintática 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);
    };
  }
}

Os padrões alternativos envolvem a vinculação de uma função existente no construtor ou a atribuição da 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 um 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 horário 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 foi definido com algum outro valor por coisas como listeners de eventos 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 usando um objeto pai (parent.func()):

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

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

Nesse caso, a função é chamada como um membro de obj, então this será obj. Isso acontece no tempo da chamada. Portanto, o link será corrompido 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 assim:

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 tipo de nó DOM, 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 rigoroso:

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.