Aprenda / JavaScript do zero

IntermediárioAula 9 de 1616 min0% da trilha

Escopo, hoisting e closures

var 'vaza' pro escopo da função inteira, hoisting não é mágica e uma função pode continuar lembrando de uma variável muito depois de ter terminado de rodar.

Você já usa let e const desde a primeira lição. Agora vale entender por que var foi abandonado — e o mecanismo por trás de um dos comportamentos mais úteis (e mais mal compreendidos) de JavaScript: a closure.

Escopo de função vs. escopo de bloco

var tem escopo de função — ela ignora blocos como if e for. let e const têm escopo de bloco — existem só dentro do { } onde foram declaradas.

function exemploVar() {
  if (true) {
    var mensagem = "declarada dentro do if";
  }
  console.log(mensagem); // "declarada dentro do if" — var vazou pro escopo da função inteira
}

function exemploLet() {
  if (true) {
    let mensagem = "declarada dentro do if";
  }
  console.log(mensagem); // ReferenceError: mensagem is not defined — não existe fora do bloco
}

Esse "vazamento" do var é a fonte clássica de bug em loop: uma variável que você achava que estava isolada dentro de cada iteração, na verdade é uma única variável compartilhada por todo mundo.

Hoisting — o que é realmente içado

Antes de rodar qualquer linha, o motor de JavaScript varre o escopo e "sobe" as declarações pro topo. Isso é o hoisting. A parte que costuma confundir: ele içar a declaração não significa içar o valor.

console.log(nome); // undefined — a declaração "var nome" foi içada, mas a atribuição não
var nome = "Ana";

console.log(idade); // ReferenceError: Cannot access 'idade' before initialization
let idade = 28;

Com var, a variável já existe (com valor undefined) antes da linha onde você a declarou. Com let/const, a variável também é içada, mas fica presa numa zona temporal morta até a linha da declaração rodar de verdade — por isso o erro, em vez de simplesmente devolver undefined.

jaFuncionava(); // "isso roda mesmo chamada antes de aparecer no arquivo"

function jaFuncionava() {
  return "isso roda mesmo chamada antes de aparecer no arquivo";
}

expressaoFuncao(); // TypeError: expressaoFuncao is not a function

const expressaoFuncao = function () {
  return "só existe a partir desta linha";
};

Declaração de função (function nome() ) é içada inteira, corpo incluído — por isso funciona chamar antes. Uma função guardada numa variável só ganha valor quando aquela linha roda, então chamar antes dá erro, do mesmo jeito que aconteceria com qualquer outra variável.

Closure — uma função que lembra de onde nasceu

Uma closure acontece quando uma função guarda acesso às variáveis do escopo onde foi criada, mesmo depois desse escopo já ter terminado de executar. Não é uma cópia do valor — é uma referência viva.

function criarContador() {
  let contagem = 0; // essa variável fica "presa" na closure

  return function incrementar() {
    contagem++;
    return contagem;
  };
}

const contador1 = criarContador();
contador1(); // 1
contador1(); // 2

const contador2 = criarContador();
contador2(); // 1 — tem a própria "contagem", independente de contador1

criarContador() já terminou de rodar há muito tempo quando você chama contador1() pela segunda vez — mas incrementar continua enxergando contagem, porque carrega consigo o escopo onde nasceu. Cada chamada de criarContador() cria uma closure nova, com sua própria cópia de contagem.

Por que isso é a base de debounce e memoization

Qualquer padrão que precisa "lembrar" de algo entre chamadas — sem usar uma variável global — depende de closure.

function debounce(fn, atraso) {
  let temporizador; // fica preso na closure, compartilhado entre todas as chamadas

  return function (...args) {
    clearTimeout(temporizador);
    temporizador = setTimeout(() => fn(...args), atraso);
  };
}

const salvarComDebounce = debounce(salvarNoServidor, 500);
// cada chamada de salvarComDebounce cancela a anterior — só a última, depois de 500ms parado, executa de verdade

Sem closure, temporizador teria que ser uma variável global (compartilhada por todo o programa, arriscando conflito com outro código) ou a função esqueceria dele a cada chamada.

Exercício

Escreva uma função criarContadorRegressivo(inicio) que devolve uma função. Cada chamada dessa função decrementa 1 e retorna o valor atual, começando de inicio.

Quiz rápido

Qual a diferença de escopo entre var e let dentro de um bloco if?
O que realmente acontece no hoisting de let idade = 28?