Hoisting é um comportamento do JavaScript onde declarações de variáveis e funções são movidas para o topo de seu escopo durante a fase de compilação, antes da execução do código. Isso significa que você pode usar uma variável ou função antes de declará-la no código fonte, mas com comportamentos diferentes dependendo da palavra-chave utilizada. Entender profundamente o hoisting é essencial para evitar bugs sutis e escrever código previsível.

Nesta aula, vamos desmistificar o hoisting para declarações com var, let, const e funções, explorar a Temporal Dead Zone (TDZ) e analisar exemplos práticos que ilustram cada conceito.

Hoisting de var, let, const e funções

O hoisting ocorre de forma diferente para cada tipo de declaração. Com var, a declaração é içada para o topo do escopo da função ou global, mas a inicialização permanece no local original. Isso significa que a variável existe (com valor undefined) desde o início do escopo. Já com let e const, a declaração também é içada, mas a variável não é inicializada até a linha da declaração ser executada, ficando em uma zona morta temporal (TDZ) entre o início do escopo e a declaração.

Para funções declaradas com a sintaxe function nome() {}, tanto a declaração quanto a definição são içadas, permitindo chamar a função antes de sua declaração. Já expressões de função (como const fn = function() {}) seguem as regras da variável usada (var, let, const), e apenas a declaração da variável é içada (se var) ou fica na TDZ (se let/const).

// Exemplo com var
console.log(a); // undefined (hoisting)
var a = 10;
console.log(a); // 10

// Exemplo com let
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

// Exemplo com const
console.log(c); // ReferenceError
const c = 30;

// Exemplo com função
foo(); // "foo"
function foo() {
  console.log("foo");
}

// Expressão de função com var
bar(); // TypeError: bar is not a function (pois bar é undefined)
var bar = function() {
  console.log("bar");
};

// Expressão de função com let
baz(); // ReferenceError
let baz = function() {
  console.log("baz");
};

Temporal Dead Zone

A Temporal Dead Zone (TDZ) é o período entre a entrada no escopo (bloco) e a declaração real da variável com let ou const. Durante a TDZ, qualquer tentativa de acessar a variável resulta em um ReferenceError. Esse comportamento garante que a variável não seja usada antes de sua inicialização, evitando bugs comuns associados ao hoisting de var.

A TDZ também se aplica a parâmetros de funções com valores padrão e a classes. Por exemplo, se você tentar acessar uma variável let dentro de um bloco antes de sua declaração, mesmo que a variável esteja no escopo, ela está na TDZ. O mesmo vale para const. É importante notar que a TDZ é estritamente temporal, não espacial: ela depende da ordem de execução, não da posição no código.

// Exemplo de TDZ
{
  // TDZ para x começa aqui
  console.log(x); // ReferenceError
  let x = 5;
  // TDZ termina aqui
}

// TDZ com typeof
{
  console.log(typeof x); // ReferenceError (diferente de var, que retorna undefined)
  let x = 10;
}

// TDZ em parâmetros padrão
function test(y = x) {
  let x = 1;
  console.log(y); // ReferenceError: Cannot access 'x' before initialization
}
test();

// TDZ em classes
const p = new Polygon(); // ReferenceError
class Polygon {}

Exemplos

Vamos consolidar os conceitos com exemplos mais elaborados que mostram as interações entre hoisting, TDZ e escopos. O primeiro exemplo ilustra como o hoisting de var pode causar comportamentos inesperados em loops, enquanto o segundo mostra a segurança de let em blocos.

// Exemplo 1: var em loop
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3 (devido ao hoisting de var)
}
console.log(i); // 3 (i vazou do loop)

// Exemplo 2: let em loop
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 0, 1, 2 (cada iteração tem seu próprio j)
}
console.log(j); // ReferenceError: j is not defined

// Exemplo 3: Hoisting de função vs var
function test() {
  console.log(foo); // undefined (hoisting de var)
  console.log(bar); // [Function: bar] (hoisting completo)
  var foo = "foo";
  function bar() {
    return "bar";
  }
}
test();

// Exemplo 4: TDZ com bloco aninhado
let x = "global";
{
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = "local";
}

Boas Práticas

Para evitar problemas com hoisting, recomenda-se declarar todas as variáveis e funções no topo do escopo (função ou bloco) de forma explícita. Utilize let e const em vez de var para ter um comportamento mais previsível e evitar vazamentos de escopo. Sempre declare variáveis antes de usá-las, e evite depender do hoisting de funções para chamadas antes da declaração, pois isso pode tornar o código menos legível.

Referências

Exercícios

  1. O que será exibido no console do código abaixo? Explique o comportamento.

    console.log(a);
    var a = 1;
    console.log(a);

    ✓ Resposta: Será exibido undefined e depois 1. Isso ocorre porque a declaração var a é içada para o topo do escopo, mas a atribuição = 1 permanece no lugar. Portanto, no primeiro console.log, a existe mas vale undefined; após a atribuição, vale 1.
  2. O que acontece ao executar o código a seguir? Justifique.

    {
      console.log(x);
      let x = 2;
    }

    ✓ Resposta: Ocorre um ReferenceError: Cannot access 'x' before initialization. A variável x com let é içada, mas entra na Temporal Dead Zone desde o início do bloco até a linha da declaração. Tentar acessá-la antes da declaração gera erro.
  3. Analise o código e diga qual será a saída:

    foo();
    function foo() {
      console.log("foo");
    }
    bar();
    var bar = function() {
      console.log("bar");
    };

    ✓ Resposta: A saída será "foo" seguido de TypeError: bar is not a function. A função foo é içada completamente, então pode ser chamada antes da declaração. Já bar é uma expressão de função atribuída a uma variável var; apenas a declaração var bar é içada (inicializada como undefined), então ao tentar chamar bar(), ela não é uma função.
  4. Explique o que é a Temporal Dead Zone e dê um exemplo que a demonstre.

    ✓ Resposta: A Temporal Dead Zone (TDZ) é o período entre o início do escopo (bloco) e a declaração de uma variável com let ou const, durante o qual a variável existe mas não pode ser acessada, resultando em ReferenceError. Exemplo:
    {
      // TDZ para x começa
      console.log(x); // ReferenceError
      let x = 10;
      // TDZ termina
    }
  5. Qual a diferença de hoisting entre uma declaração de função (function nome() {}) e uma expressão de função atribuída a uma variável let?

    ✓ Resposta: Na declaração de função, tanto a declaração quanto o corpo da função são içados, permitindo chamá-la antes da linha de definição. Já na expressão de função com let, apenas a declaração da variável é içada (mas fica na TDZ até a linha de declaração), e a atribuição da função ocorre no local. Portanto, tentar chamá-la antes da declaração resulta em ReferenceError (devido à TDZ), e não em TypeError como com var.