Em JavaScript, a palavra-chave this é uma das fontes mais comuns de confusão, mas também uma das mais poderosas. Diferente de outras linguagens, o valor de this não é definido pelo escopo léxico, mas sim pela forma como a função é chamada. Compreender as regras de ligação de this é essencial para escrever código previsível e evitar bugs sutis. Nesta aula, vamos mergulhar nas quatro regras de ligação, explorar os métodos call, apply e bind, e entender como as arrow functions mudam completamente o jogo.

Antes de continuar, é importante destacar que this é avaliado em tempo de execução, não em tempo de escrita. Isso significa que a mesma função pode ter valores diferentes de this dependendo de onde e como é invocada. Vamos desvendar esses mistérios com exemplos práticos e exercícios que reforçam cada conceito.

Regras de ligação

O valor de this é determinado por quatro regras, em ordem de precedência: ligação implícita, ligação explícita, ligação new e ligação padrão. Vamos examinar cada uma.

1. Ligação implícita: Quando uma função é chamada como método de um objeto, o this dentro da função refere-se a esse objeto. Por exemplo:

const pessoa = {
  nome: 'João',
  saudacao() {
    console.log(`Olá, eu sou ${this.nome}`);
  }
};

pessoa.saudacao(); // Olá, eu sou João

2. Ligação explícita: Usando call, apply ou bind, podemos definir explicitamente o valor de this (veremos detalhes na próxima seção).

3. Ligação new: Quando usamos o operador new para criar um objeto a partir de uma função construtora, o this dentro da função aponta para o novo objeto criado.

function Carro(marca) {
  this.marca = marca;
}

const meuCarro = new Carro('Fiat');
console.log(meuCarro.marca); // Fiat

4. Ligação padrão: Se nenhuma das regras anteriores se aplicar, o this assume o valor global (em modo estrito, undefined). Em navegadores, o objeto global é window; no Node.js, é global.

function mostrarThis() {
  console.log(this);
}

mostrarThis(); // window (ou undefined em strict mode)

A ordem de precedência é: new > ligação explícita > ligação implícita > ligação padrão. Vamos ver um exemplo que combina várias regras:

const obj = {
  valor: 42,
  funcao() {
    console.log(this.valor);
  }
};

obj.funcao.call({ valor: 99 }); // 99 (ligação explícita vence a implícita)
new obj.funcao(); // undefined (this é um novo objeto vazio)

call, apply e bind

Os métodos call, apply e bind são usados para controlar explicitamente o valor de this em uma função. Eles são propriedades de todas as funções JavaScript.

call e apply invocam a função imediatamente, com um this especificado e argumentos. A diferença está na forma de passar os argumentos: call recebe uma lista de argumentos separados por vírgula, enquanto apply recebe um array de argumentos.

function apresentar(prefixo, sufixo) {
  console.log(`${prefixo} ${this.nome} ${sufixo}`);
}

const pessoa = { nome: 'Maria' };

apresentar.call(pessoa, 'Olá', '!'); // Olá Maria !
apresentar.apply(pessoa, ['Oi', '!!']); // Oi Maria !!

bind não invoca a função imediatamente; em vez disso, retorna uma nova função com o this fixado. Isso é útil para criar funções com contexto pré-definido, especialmente em callbacks ou event handlers.

const pessoa = {
  nome: 'Carlos',
  saudacao: function() {
    console.log(`Olá, meu nome é ${this.nome}`);
  }
};

const saudacao = pessoa.saudacao.bind(pessoa);
saudacao(); // Olá, meu nome é Carlos

Além de fixar this, bind também pode pré-definir argumentos, criando funções parciais:

function multiplicar(a, b) {
  return a * b;
}

const dobrar = multiplicar.bind(null, 2);
console.log(dobrar(5)); // 10

this em arrow vs função normal

As arrow functions não têm seu próprio this. Em vez disso, elas herdam o this do escopo léxico (o escopo onde foram definidas). Isso é uma diferença crucial em relação às funções normais, que recebem o this conforme a chamada.

Considere o exemplo clássico de um temporizador:

function Contador() {
  this.valor = 0;
  setInterval(function() {
    this.valor++; // this é o objeto global, não Contador
    console.log(this.valor); // NaN
  }, 1000);
}

new Contador();

Com uma arrow function, o this é capturado do escopo da função Contador, que é a instância criada:

function Contador() {
  this.valor = 0;
  setInterval(() => {
    this.valor++; // this é a instância de Contador
    console.log(this.valor); // 1, 2, 3...
  }, 1000);
}

new Contador();

Outra diferença: arrow functions não podem ser usadas como construtoras (com new) e não têm a propriedade prototype. Além disso, os métodos call, apply e bind não podem alterar o this de uma arrow function, pois ela ignora qualquer valor explicitamente definido.

const arrow = () => console.log(this);
arrow.call({ nome: 'Teste' }); // this continua sendo o escopo global (ou undefined em strict)

Portanto, a escolha entre arrow e função normal depende do contexto: use arrow functions quando precisar do this do escopo externo, e funções normais quando precisar de um this dinâmico (como em métodos de objeto).

Boas práticas e observações finais

  • Use bind ao passar métodos como callbacks para garantir que o this seja o esperado.
  • Prefira arrow functions em callbacks que dependem do contexto externo, como em setTimeout ou event listeners, para evitar o problema do this perdido.
  • Em classes, os métodos são ligados à instância automaticamente, mas cuidado ao extraí-los; use bind ou arrow functions como propriedades de classe (em Babel/TypeScript) para manter o this correto.
  • Em módulos ES6, o escopo global não é window; em navegadores, ainda é, mas em Node.js é global. Em código estrito, o this global é undefined em funções simples.
  • Evite usar this em funções que serão chamadas de maneiras diferentes; prefira parâmetros explícitos quando possível.

Referências

Exercícios

  1. Explique qual será o valor de this em cada chamada e por quê:
    const obj = {
      nome: 'objeto',
      metodo() { return this.nome; }
    };
    const fn = obj.metodo;
    console.log(obj.metodo());
    console.log(fn());

    ✓ Resposta: obj.metodo() retorna 'objeto' (ligação implícita, pois é chamada como método). fn() retorna undefined (ou erro em strict mode) porque a função é chamada sem contexto, então this é o objeto global (ou undefined), e this.nome é indefinido.
  2. Escreva uma função que aceite um número e use call para passar um objeto com uma propriedade multiplicador e retorne o produto.
  3. ✓ Resposta:
    function calcularMultiplo(num) {
      return num * this.multiplicador;
    }
    const contexto = { multiplicador: 3 };
    const resultado = calcularMultiplo.call(contexto, 5);
    console.log(resultado); // 15
  4. Crie uma função que use bind para fixar o this a um objeto e depois chame a função resultante para exibir uma mensagem.
  5. ✓ Resposta:
    const usuario = { nome: 'Ana', saudacao: function() { console.log('Olá, ' + this.nome); } };
    const saudar = usuario.saudacao.bind(usuario);
    saudar(); // Olá, Ana
  6. Converta a seguinte função para usar uma arrow function e explique o que acontece com o this:
    function Timer() {
      this.segundos = 0;
      setInterval(function() {
        this.segundos++;
        console.log(this.segundos);
      }, 1000);
    }
    new Timer();
  7. ✓ Resposta: Converta para:
    function Timer() {
      this.segundos = 0;
      setInterval(() => {
        this.segundos++;
        console.log(this.segundos);
      }, 1000);
    }
    new Timer();
    A arrow function captura o this do escopo de Timer, que é a instância criada, então this.segundos incrementa corretamente.
  8. Explique por que o código a seguir imprime undefined e como corrigi-lo usando bind:
    const carro = {
      marca: 'Toyota',
      exibirMarca: function() {
        console.log(this.marca);
      }
    };
    setTimeout(carro.exibirMarca, 1000);
  9. ✓ Resposta: O this dentro de exibirMarca não é o objeto carro quando passado como callback, porque o setTimeout chama a função sem contexto, fazendo o this ser o objeto global (ou undefined). A correção: setTimeout(carro.exibirMarca.bind(carro), 1000); ou usar arrow function: setTimeout(() => carro.exibirMarca(), 1000);