JavaScript: o que isso significa?

Descobrir o valor de this pode ser complicado em JavaScript. Veja como fazer isso...

Jake archibald
Jake Archibald

A this do JavaScript é o motivo de muitas piadas, e isso é muito complicado. No entanto, já vi desenvolvedores fazendo coisas muito mais complicadas e específicas de domínio para evitar lidar com esse this. Se você tiver dúvidas sobre this, esperamos que 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 é semelhante a uma if (…) … else if () … else if (…) … grande, então você pode ir direto para a primeira seção que corresponde ao código que está visualizando.

  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 "vinculado"
  4. Caso contrário, se this for definido no momento da chamada
  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 estiver definida como uma função de seta:

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

Nesse caso, o valor de this é sempre o mesmo que 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 mudado. 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 "Vinculado"

Com os 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 da 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).

O exemplo acima pode parecer que está quebrando a regra "this será igual a 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);
    };
  }
}

Os padrões alternativos envolvem vincular uma função existente no construtor ou atribuir a função no construtor. Se você não puder usar os 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 comando acima chamará Whatever (ou a função de construtor, 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 de estilo mais antigo:

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 "vinculado":

function someFunction() {
  return this;
}

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

Sempre que boundFunction for chamado, o valor this 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 mudado 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 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, o this é definido como 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 acima. 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, então 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 como:

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

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

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

Curiosidade: nem todas as APIs usam this internamente. Os métodos do console, como console.log, foram modificados para evitar referências de this. Portanto, log não precisa estar vinculado a console

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

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

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

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

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. Alguma pergunta? Perdi alguma coisa? Fique à vontade para twittar para mim.

Agradecemos a Mathias Bynens, Ingvar Stepanyan e Thomas Steiner pela análise.