Closures em JavaScript: Escopo Léxico e Funções de Primeira Classe na Prática Já leu

Escopo Léxico: O Alicerce das Closures O escopo léxico é a regra fundamental que governa como as variáveis são resolvidas em JavaScript. Ele significa que o escopo é determinado pela posição do código no arquivo, não por onde a função é chamada. Quando você declara uma função, ela "lembra" do ambiente em que foi definida, criando uma cadeia de escopos que pode ser consultada em tempo de execução. Neste exemplo, acessa variáveis de três níveis diferentes: seu próprio escopo, o escopo de e o escopo global. O JavaScript busca essas variáveis seguindo a cadeia de escopos, sempre começando pelo mais próximo. Essa característica é chamada de shadowing quando uma variável interna tem o mesmo nome de uma externa — a interna prevalece. Closures: Capturando o Escopo Uma closure é uma função que "captura" variáveis do seu escopo envolvente e as mantém vivas, mesmo após a função externa ter terminado sua execução. Contrário ao que muitos pensam, closures não são um

Escopo Léxico: O Alicerce das Closures

O escopo léxico é a regra fundamental que governa como as variáveis são resolvidas em JavaScript. Ele significa que o escopo é determinado pela posição do código no arquivo, não por onde a função é chamada. Quando você declara uma função, ela "lembra" do ambiente em que foi definida, criando uma cadeia de escopos que pode ser consultada em tempo de execução.

const global = "Sou global";

function externa() {
  const doEscopo = "Sou do escopo da função externa";

  function interna() {
    const local = "Sou local";
    console.log(local);           // "Sou local"
    console.log(doEscopo);        // "Sou do escopo da função externa"
    console.log(global);          // "Sou global"
  }

  interna();
}

externa();

Neste exemplo, interna() acessa variáveis de três níveis diferentes: seu próprio escopo, o escopo de externa() e o escopo global. O JavaScript busca essas variáveis seguindo a cadeia de escopos, sempre começando pelo mais próximo. Essa característica é chamada de shadowing quando uma variável interna tem o mesmo nome de uma externa — a interna prevalece.

Closures: Capturando o Escopo

Uma closure é uma função que "captura" variáveis do seu escopo envolvente e as mantém vivas, mesmo após a função externa ter terminado sua execução. Contrário ao que muitos pensam, closures não são um recurso especial — elas são o comportamento padrão de toda função em JavaScript.

function contador() {
  let count = 0;

  return function() {
    count++;
    return count;
  };
}

const incrementar = contador();
console.log(incrementar()); // 1
console.log(incrementar()); // 2
console.log(incrementar()); // 3

Aqui, a função retornada é uma closure que captura a variável count. Mesmo após contador() ter terminado, a variável count não é descartada pelo garbage collector — ela continua viva enquanto a função retornada existir. Cada chamada a incrementar() modifica e retorna o novo valor de count. Se você criar outra variável com contador(), ela terá sua própria instância isolada de count.

const contador1 = contador();
const contador2 = contador();

console.log(contador1()); // 1
console.log(contador1()); // 2
console.log(contador2()); // 1 — nova instância!

Funções de Primeira Classe e Padrões Práticos

Em JavaScript, funções são cidadãs de primeira classe: podem ser atribuídas a variáveis, passadas como argumentos e retornadas como valores. Essa característica, combinada com closures, permite padrões poderosos como factory functions, decorators e módulos.

Factory Functions

Factory functions retornam novos objetos, frequentemente usando closures para encapsular dados privados:

function criarPessoa(nome, idade) {
  // Dados privados
  let _idade = idade;

  return {
    getNome() { return nome; },
    getIdade() { return _idade; },
    fazerAniversario() { _idade++; }
  };
}

const joao = criarPessoa("João", 30);
console.log(joao.getNome());       // "João"
console.log(joao.getIdade());      // 30
joao.fazerAniversario();
console.log(joao.getIdade());      // 31

Neste padrão, _idade é privada — só pode ser acessada através dos métodos retornados. Isso é encapsulamento real sem necessidade de classes.

Decorators

Decorators usam closures para envolver funções e modificar seu comportamento:

function logger(funcao) {
  return function(...args) {
    console.log(`Chamando ${funcao.name} com:`, args);
    const resultado = funcao.apply(this, args);
    console.log(`Resultado:`, resultado);
    return resultado;
  };
}

function somar(a, b) {
  return a + b;
}

const somarComLog = logger(somar);
somarComLog(5, 3);
// Chamando somar com: [5, 3]
// Resultado: 8

O decorator logger retorna uma closure que captura a função original, permitindo executar código antes e depois dela. Esse padrão é usado extensivamente em frameworks modernos.

Padrão de Módulo

Closures são a base do padrão de módulo, que cria espaços privados sem poluir o escopo global:

const calculadora = (function() {
  // Dados e funções privadas
  const historico = [];

  function registrar(operacao) {
    historico.push(operacao);
  }

  // API pública
  return {
    somar(a, b) {
      const resultado = a + b;
      registrar(`${a} + ${b} = ${resultado}`);
      return resultado;
    },
    getHistorico() {
      return [...historico];
    }
  };
})();

console.log(calculadora.somar(2, 3));        // 5
console.log(calculadora.getHistorico());     // ["2 + 3 = 5"]
// console.log(calculadora.historico);       // undefined — privado!

A função anônima auto-executada cria um escopo privado. Apenas os métodos retornados têm acesso a historico e registrar, implementando verdadeiro encapsulamento.

Armadilhas Comuns e Boas Práticas

Um erro frequente ocorre em loops quando você tenta capturar valores mutáveis. No exemplo abaixo, todas as closures compartilham a mesma variável i:

// ❌ Errado
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(() => console.log(i));
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3

Ao invocar as funções, i já vale 3. A solução é usar let ou criar uma closure adicional:

// ✅ Correto com let
const funcs = [];
for (let i = 0; i < 3; i++) {
  funcs.push(() => console.log(i));
}
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

// ✅ Ou com IIFE (função auto-executada)
const funcs2 = [];
for (var i = 0; i < 3; i++) {
  funcs2.push((function(j) {
    return () => console.log(j);
  })(i));
}
funcs2[0](); // 0

Com let, cada iteração cria um novo escopo, capturando uma nova instância de i. Com IIFE, passamos i como argumento, criando uma closure sobre um parâmetro que não muda.

Conclusão

Closures são o mecanismo fundamental que permite encapsulamento, abstrações seguras e padrões de design poderosos em JavaScript. O escopo léxico garante que funções sempre acessem o ambiente correto, independente de onde são executadas. Quando você domina closures e funções de primeira classe, pode criar código mais modular, testável e mantível — desde factory functions até decorators e módulos auto-contidos. A chave é entender que toda função em JavaScript é uma closure, e aprender a usá-las intencionalmente para resolver problemas de forma elegante.

Referências


Artigos relacionados