Escopo e closures são conceitos fundamentais em JavaScript que definem como variáveis são acessadas e como funções capturam seu contexto de criação. Compreender esses tópicos é essencial para escrever código previsível, evitar bugs sutis e dominar padrões como módulos e callbacks. Nesta aula, abordaremos desde a teoria do escopo léxico até as pegadinhas mais comuns com closures.

O escopo léxico (ou estático) determina a visibilidade de variáveis com base na posição das funções no código-fonte. Já closures são funções que 'lembram' do escopo onde foram criadas, mesmo após esse escopo ter sido executado. Vamos mergulhar em cada um desses tópicos com exemplos práticos.

Escopo léxico

Escopo léxico significa que o escopo de uma variável é definido pela sua localização no código-fonte durante a fase de compilação (antes da execução). Em JavaScript, funções criam um novo escopo, e variáveis declaradas com let e const têm escopo de bloco, enquanto var tem escopo de função.

Quando uma função é definida dentro de outra, a função interna pode acessar variáveis da função externa, mas o contrário não é verdadeiro. Isso cria uma cadeia de escopos (scope chain).

let x = 1; // escopo global

function outer() {
  let y = 2; // escopo de outer

  function inner() {
    let z = 3; // escopo de inner
    console.log(x + y + z); // acessa x (global), y (outer), z (inner)
  }

  inner();
}

outer(); // 6
// console.log(y); // ReferenceError: y is not defined

No exemplo acima, inner tem acesso a y de outer e a x global. Isso é possível porque o escopo é determinado pela estrutura do código, não pela execução.

O que é um closure

Um closure é uma função que mantém uma referência ao seu escopo léxico, mesmo quando a função é executada fora desse escopo. Em outras palavras, é uma função que 'lembra' do ambiente onde foi criada, incluindo variáveis locais que estavam no escopo naquele momento.

Em JavaScript, closures são criados toda vez que uma função é definida dentro de outra função e a função interna é retornada ou passada como argumento. A função interna 'fecha' o escopo externo, preservando as variáveis.

function criarContador() {
  let contador = 0;
  return function() {
    contador++;
    return contador;
  };
}

const meuContador = criarContador();
console.log(meuContador()); // 1
console.log(meuContador()); // 2
console.log(meuContador()); // 3

// A variável contador não é acessível diretamente
// console.log(contador); // ReferenceError

A função retornada por criarContador é um closure. Ela mantém acesso à variável contador mesmo após criarContador ter terminado. Cada chamada a meuContador incrementa o mesmo contador.

Casos de uso reais

Closures são amplamente utilizados em JavaScript para encapsulamento, funções de fábrica, callbacks e módulos. Um exemplo comum é o padrão de módulo, onde variáveis privadas são expostas através de métodos públicos.

function criarCarteira() {
  let saldo = 0;
  return {
    depositar: function(valor) {
      saldo += valor;
    },
    sacar: function(valor) {
      if (valor <= saldo) {
        saldo -= valor;
      } else {
        console.log('Saldo insuficiente');
      }
    },
    obterSaldo: function() {
      return saldo;
    }
  };
}

const minhaCarteira = criarCarteira();
minhaCarteira.depositar(100);
minhaCarteira.sacar(30);
console.log(minhaCarteira.obterSaldo()); // 70
// console.log(minhaCarteira.saldo); // undefined

Outro caso é em funções de alta ordem, como setTimeout ou event listeners, onde closures preservam o estado no momento da criação.

function criarSaudacao(nome) {
  return function() {
    console.log('Olá, ' + nome);
  };
}

const saudacaoJoao = criarSaudacao('João');
setTimeout(saudacaoJoao, 1000); // após 1s: 'Olá, João'

Também são essenciais em loops com var (antes do let), para capturar o valor correto da iteração.

Pegadinhas

Uma pegadinha clássica com closures ocorre quando criamos funções dentro de um loop usando var. Como var tem escopo de função, todas as funções compartilham a mesma variável, resultando em valores inesperados.

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // imprime 3, 3, 3
  }, 100);
}

Isso acontece porque o closure captura a variável i (que é a mesma para todas as iterações) e, quando o timeout executa, o loop já terminou e i vale 3. A solução é usar let (que tem escopo de bloco) ou criar um novo escopo com uma IIFE.

// Solução com let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2
  }, 100);
}

// Solução com IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 0, 1, 2
    }, 100);
  })(i);
}

Outra pegadinha é o vazamento de memória: closures que mantêm referências a objetos grandes podem impedir a coleta de lixo, causando consumo excessivo de memória. Sempre que possível, evite armazenar referências desnecessárias em closures.

Boas práticas

Prefira let e const a var para evitar problemas de escopo. Use closures para encapsulamento, mas tome cuidado com o consumo de memória em closures de longa duração. Em loops, capture o valor com let ou IIFE para evitar o problema clássico.

Referências

Exercícios

  1. Crie uma função criarMultiplicador que recebe um número n e retorna uma função que multiplica seu argumento por n. Teste com const dobrar = criarMultiplicador(2); console.log(dobrar(5));.

    ✓ Resposta:
    function criarMultiplicador(n) {
      return function(x) {
        return x * n;
      };
    }
    
    const dobrar = criarMultiplicador(2);
    console.log(dobrar(5)); // 10
  2. Explique por que o código abaixo imprime 3, 3, 3 e corrija-o usando let.

    for (var i = 0; i < 3; i++) {
      setTimeout(function() {
        console.log(i);
      }, 100);
    }

    ✓ Resposta:

    O código imprime 3, 3, 3 porque var i tem escopo de função, não de bloco. O closure captura a mesma variável i, que após o loop vale 3. Corrigindo com let:

    for (let i = 0; i < 3; i++) {
      setTimeout(function() {
        console.log(i);
      }, 100);
    }
  3. Implemente uma função criarSaudacao que recebe um nome e retorna uma função que, ao ser chamada, exibe 'Olá, [nome]'. Use closures.

    ✓ Resposta:
    function criarSaudacao(nome) {
      return function() {
        console.log('Olá, ' + nome);
      };
    }
    
    const saudacaoMaria = criarSaudacao('Maria');
    saudacaoMaria(); // 'Olá, Maria'
  4. Qual é a saída do código abaixo? Explique.

    function outer() {
      let x = 10;
      function inner() {
        return x;
      }
      x = 20;
      return inner;
    }
    
    const fn = outer();
    console.log(fn());

    ✓ Resposta:

    A saída é 20. O closure inner captura a variável x por referência, não por valor. Quando outer é executada, x é alterada para 20 antes de retornar inner. Portanto, fn() retorna o valor atual de x, que é 20.

  5. Crie uma função criarContador que aceita um valor inicial e retorna um objeto com métodos incrementar, decrementar e obterValor. Use closures para manter o valor privado.

    ✓ Resposta:
    function criarContador(inicial = 0) {
      let valor = inicial;
      return {
        incrementar: function() {
          valor++;
        },
        decrementar: function() {
          valor--;
        },
        obterValor: function() {
          return valor;
        }
      };
    }
    
    const contador = criarContador(10);
    contador.incrementar();
    contador.incrementar();
    contador.decrementar();
    console.log(contador.obterValor()); // 11