Hoisting é um comportamento padrão do JavaScript onde declarações de variáveis e funções são movidas para o topo de seus escopos 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 o comportamento varia conforme o tipo de declaração. Compreender hoisting é essencial para evitar bugs sutis e escrever código previsível.

Nesta aula, vamos mergulhar fundo no hoisting de var, let, const e funções, entender a Temporal Dead Zone (TDZ) e ver exemplos que ilustram esses conceitos na prática.

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

O hoisting ocorre de forma diferente para cada tipo de declaração. Variáveis declaradas com var são içadas e inicializadas com undefined. Isso significa que você pode acessá-las antes da declaração, mas o valor será undefined até que a atribuição ocorra. Já as declarações com let e const também são içadas, mas não são inicializadas. Elas entram na chamada Temporal Dead Zone (TDZ) desde o início do escopo até o ponto da declaração, e qualquer tentativa de acesso antes da declaração resulta em um erro ReferenceError.

Funções declaradas com a sintaxe function são completamente içadas, incluindo o corpo da função. Portanto, você pode chamá-las antes da declaração. Já as expressões de função (como const fn = function() {}) seguem as regras da variável usada (neste caso, const), ou seja, o hoisting ocorre mas a variável não é inicializada, e a função só estará disponível após a atribuição.

// Exemplo com var
console.log(a); // undefined
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: Cannot access 'c' before initialization
const c = 30;

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

// Exemplo com expressão de função
bar(); // TypeError: bar is not a function (ou ReferenceError dependendo do hoisting)
const bar = function() {
  console.log("Expressão de função");
};

Temporal Dead Zone

A Temporal Dead Zone (TDZ) é o período entre a entrada no escopo (por exemplo, o início de um bloco) e a declaração real da variável com let ou const. Durante a TDZ, a variável existe no escopo, mas não pode ser acessada, resultando em um ReferenceError. A TDZ existe para evitar que variáveis sejam usadas antes de sua inicialização, promovendo um código mais seguro.

Um exemplo clássico de TDZ ocorre em blocos if ou loops for. No código abaixo, a variável x está na TDZ até a linha da declaração:

{
  // Início da TDZ para x
  console.log(x); // ReferenceError
  let x = 5;
  // Fim da TDZ, x pode ser acessado
  console.log(x); // 5
}

É importante notar que a TDZ também afeta variáveis const da mesma forma, com a diferença adicional de que const exige inicialização na declaração e não pode ser reatribuída.

Exemplos

Vamos analisar alguns exemplos práticos que ilustram o hoisting e a TDZ em ação.

Exemplo 1: Hoisting de var em loops

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Saída: 3, 3, 3 (devido ao hoisting de var, i tem escopo de função)

Como var tem escopo de função (ou global), a variável i é içada para o escopo da função (ou global) e é compartilhada entre todas as iterações. Quando os timeouts executam, o loop já terminou e i vale 3. Para corrigir, use let:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Saída: 0, 1, 2 (cada iteração tem seu próprio escopo de bloco)

Exemplo 2: TDZ com typeof

Usar typeof em uma variável na TDZ também lança um erro, ao contrário de variáveis não declaradas que retornam "undefined":

console.log(typeof undeclaredVar); // "undefined" (variável não declarada)
console.log(typeof letVar); // ReferenceError: Cannot access 'letVar' before initialization
let letVar = 10;

Exemplo 3: Hoisting de funções e expressões

// Função declarada: hoisting completo
sayHello(); // "Hello!"
function sayHello() {
  console.log("Hello!");
}

// Expressão de função: hoisting da variável (const) mas não da função
sayGoodbye(); // ReferenceError: Cannot access 'sayGoodbye' before initialization
const sayGoodbye = function() {
  console.log("Goodbye!");
};

Boas práticas

Para evitar surpresas com hoisting, declare todas as variáveis no topo de seus escopos (função ou bloco). Prefira const por padrão e let quando precisar reatribuir. Evite var sempre que possível, pois seu comportamento de hoisting pode causar confusão. Além disso, evite depender do hoisting de funções para chamá-las antes da declaração; embora funcione, pode tornar o código menos legível. Mantenha as declarações de funções no início do escopo.

Referências

Exercícios

  1. Qual será a saída do código abaixo? Explique o comportamento.
    console.log(a);
    var a = 5;
    console.log(a);
    

    ✓ Resposta: A saída será undefined e depois 5. A declaração var a é içada para o topo do escopo, e a variável é inicializada com undefined. O primeiro console.log exibe undefined. Em seguida, a atribuição a = 5 ocorre, e o segundo console.log exibe 5.
  2. O que acontece se tentarmos acessar uma variável let antes de sua declaração? Dê um exemplo.

    ✓ Resposta: A tentativa resulta em um ReferenceError, pois a variável está na Temporal Dead Zone. Exemplo:
    console.log(x); // ReferenceError: Cannot access 'x' before initialization
    let x = 10;
    
  3. Qual a diferença de hoisting entre uma função declarada e uma expressão de função atribuída a uma variável const?

    ✓ Resposta: A função declarada é completamente içada, podendo ser chamada antes da declaração. Já a expressão de função atribuída a uma variável const segue o hoisting de const: a variável é içada mas não inicializada, ficando na TDZ. Tentar chamá-la antes da atribuição resulta em ReferenceError.
  4. O código a seguir gera erro? Se sim, qual e por quê?
    {
      console.log(foo);
      const foo = "bar";
    }
    

    ✓ Resposta: Sim, gera um ReferenceError: Cannot access 'foo' before initialization. A variável foo declarada com const está na TDZ desde o início do bloco até a linha da declaração. O console.log tenta acessá-la antes da inicialização, causando o erro.
  5. Explique o que é a Temporal Dead Zone e por que ela existe.

    ✓ Resposta: A Temporal Dead Zone (TDZ) é o período entre a entrada no escopo (por exemplo, o início de um bloco) e a declaração de uma variável com let ou const. Durante a TDZ, a variável não pode ser acessada, e qualquer tentativa resulta em ReferenceError. A TDZ existe para evitar o uso de variáveis antes de sua inicialização, tornando o código mais previsível e seguro, ao contrário do comportamento de var que permite acesso a undefined.