Escopo e closures são conceitos fundamentais em JavaScript que determinam como e onde variáveis podem ser acessadas. O escopo define a visibilidade das variáveis, enquanto closures permitem que funções "lembrem" do ambiente onde foram criadas, mesmo após esse ambiente ter sido destruído. Nesta aula, vamos explorar esses tópicos em profundidade, com exemplos práticos e situações reais.

Entender escopo e closures é essencial para escrever código JavaScript robusto e evitar bugs sutis. Esses conceitos estão na base de padrões como módulos, factory functions e callbacks. Vamos começar pelo escopo léxico.

Escopo léxico

Escopo léxico (ou estático) significa que o escopo de uma variável é determinado pela sua posição no código-fonte, ou seja, onde ela é declarada. Em JavaScript, cada função cria seu próprio escopo, e funções aninhadas podem acessar variáveis dos escopos externos. Isso é conhecido como cadeia de escopos (scope chain).

Por exemplo, considere o código abaixo:

let global = "global";

function externa() {
  let externaVar = "externa";

  function interna() {
    let internaVar = "interna";
    console.log(global);       // "global"
    console.log(externaVar);   // "externa"
    console.log(internaVar);   // "interna"
  }

  interna();
}

externa();

Aqui, a função interna tem acesso às variáveis do seu próprio escopo, do escopo de externa e do escopo global. A cadeia de escopos é resolvida no momento da definição da função, não da execução. Isso é o escopo léxico.

É importante notar que variáveis declaradas com let e const têm escopo de bloco, enquanto var tem escopo de função. O escopo léxico se aplica a todas elas.

O que é um closure

Um closure é uma função que "lembra" do seu escopo léxico, mesmo quando a função é executada fora desse escopo. Em outras palavras, um closure é uma função que mantém referências a variáveis do escopo onde foi criada, mesmo após a função externa ter terminado.

Toda função em JavaScript é um closure, mas o termo é geralmente usado para funções que acessam variáveis de escopos externos após o retorno da função externa. Exemplo:

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

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

A função anônima retornada por criarContador é um closure. Ela mantém uma referência à variável contador do escopo de criarContador. Mesmo após criarContador ter terminado, contador ainda existe e pode ser incrementado.

Casos de uso reais

Closures são amplamente utilizados em JavaScript. Alguns casos comuns:

  • Factory functions: Funções que criam e retornam outras funções com comportamento personalizado.
  • Módulos: Padrão de módulo que encapsula dados privados e expõe uma API pública.
  • Callbacks e event handlers: Closures permitem que callbacks acessem variáveis do escopo onde foram definidos.
  • Funções de alta ordem: Como Array.map, filter, etc., muitas vezes usam closures.

Exemplo de módulo com closure:

function criarModulo() {
  let privado = "dado privado";
  return {
    getPrivado: function() {
      return privado;
    },
    setPrivado: function(valor) {
      privado = valor;
    }
  };
}

const modulo = criarModulo();
console.log(modulo.getPrivado()); // "dado privado"
modulo.setPrivado("novo valor");
console.log(modulo.getPrivado()); // "novo valor"

Outro caso é a criação de funções com estado, como contadores ou acumuladores.

Pegadinhas

Closures podem causar comportamentos inesperados, especialmente em loops. O exemplo clássico:

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

Aqui, todas as funções de callback compartilham a mesma variável i (com var), que após o loop vale 5. Para corrigir, usamos let (que cria um novo escopo a cada iteração) ou um closure adicional:

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}
// Imprime 0, 1, 2, 3, 4

// Ou com closure:
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 100);
  })(i);
}

Outra pegadinha é o vazamento de variáveis em closures que retêm referências a objetos grandes, causando memory leaks. É importante gerenciar o ciclo de vida das variáveis.

Boas práticas

  • Prefira let e const a var para evitar confusões de escopo.
  • Use closures para encapsulamento e criação de funções com estado.
  • Evite closures desnecessárias que possam reter referências a objetos grandes.
  • Em loops, utilize let ou IIFE para criar escopo por iteração.

Referências

Exercícios

  1. Crie uma função criarSaudacao que recebe um nome e retorna uma função que, ao ser chamada, exibe "Olá, [nome]!". Teste com diferentes nomes.

    ✓ Resposta:
    function criarSaudacao(nome) {
      return function() {
        console.log(`Olá, ${nome}!`);
      };
    }
    
    const saudacaoJoao = criarSaudacao("João");
    saudacaoJoao(); // Olá, João!
    const saudacaoMaria = criarSaudacao("Maria");
    saudacaoMaria(); // Olá, Maria!
  2. Explique o que será impresso no console e por quê:

    let x = 10;
    function foo() {
      console.log(x);
    }
    function bar() {
      let x = 20;
      foo();
    }
    bar();

    ✓ Resposta: Será impresso 10. A função foo foi definida no escopo global, então seu escopo léxico é o global, independentemente de onde é chamada. Portanto, x no console é a variável global x (10), não a local de bar.
  3. Escreva uma função criarMultiplicador que recebe um número n e retorna uma função que multiplica seu argumento por n.

    ✓ Resposta:
    function criarMultiplicador(n) {
      return function(x) {
        return x * n;
      };
    }
    
    const duplicar = criarMultiplicador(2);
    console.log(duplicar(5)); // 10
    const triplicar = criarMultiplicador(3);
    console.log(triplicar(5)); // 15
  4. Corrija o código abaixo para que imprima 0, 1, 2, 3, 4 (cada um após 1 segundo):

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

    ✓ Resposta: Usando let ou IIFE. Exemplo com let:
    for (let i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000);
    }
    Ou com IIFE:
    for (var i = 0; i < 5; i++) {
      (function(j) {
        setTimeout(function() {
          console.log(j);
        }, 1000);
      })(i);
    }
  5. Crie uma função criarContadorComPasso que aceita um passo (incremento) e retorna um objeto com métodos incrementar e obterValor. O contador deve começar em 0.

    ✓ Resposta:
    function criarContadorComPasso(passo) {
      let valor = 0;
      return {
        incrementar: function() {
          valor += passo;
        },
        obterValor: function() {
          return valor;
        }
      };
    }
    
    const contador = criarContadorComPasso(5);
    contador.incrementar();
    console.log(contador.obterValor()); // 5
    contador.incrementar();
    console.log(contador.obterValor()); // 10