DEV Community

João Gugel
João Gugel

Posted on • Edited on

Closures no Javascript

Closure é uma função que “lembra” do escopo onde foi criada, mantendo o acesso às variáveis desse escopo mesmo depois da função externa terminar. Assim, ela pode guardar e atualizar valores ao longo do tempo.

Exemplo de closure:

const completeUsername = () => {
    const username = "João Vitor Gugel";

    const formatName = () => {
        const names = username.split(" ");
        return `${names[0]} ${names[names.length - 1]}`;
    };

    return formatName;
};

const myClosure = completeUsername();

console.log(myClosure());
Enter fullscreen mode Exit fullscreen mode

Perceba que a função formatName tem acesso a variável username, mas a variável username não fica exposta fora da função, ela está protegida e apenas o que está dentro da função externa (completeUsername) tem acesso a esses dados.

No Javascript, as funções aninhadas tem acesso às variáveis declaradas no seu escopo externo.

Como uma closure guarda os valores

Ao escrever e executar uma função em Javascript, espera-se que o comportamento normal seja limpar os dados que pertencem apenas ao escopo dessa função quando ela terminar de ser executada, então os dados armazenados durante o processamento da função são removidos da memória, certo?

Com uma closure fica um pouco diferente. Ao declarar uma closure (uma função dentro de outra) a função interna mantém acesso às variáveis da função externa, mesmo que ela já tenha sido executada, então os dados continuam válidos sempre que a closure for chamada.

Como funciona? Veja um exemplo.

const contador = (numero: number) => {
    return () => {
        numero++;
        console.log(numero);
    };
};

const contador1 = contador(5); // Começa em 5

contador1() // Vira 6 
contador1() // Vira 7
Enter fullscreen mode Exit fullscreen mode

Criamos uma função contador que retorna uma closure responsável por incrementar o valor da variável numero, que está em seu escopo léxico.

Essa função é iniciada com o valor 5 e, ao chamarmos a função retornada (a closure), ela mantém o acesso ao valor original passado para contador. Assim, toda vez que executamos contador1(), incrementamos o número anterior, que continua salvo na memória graças à closure. O valor não é perdido entre as chamadas, mesmo que a função contador já tenha terminado sua execução.

Aplicando uma closure mais complexa

Vamos criar uma função que retorna uma closure para verificar se um usuário tem permissão para executar uma determinada ação.

const criarSistemaDePermissao = (cargo: "administrador" | "leitor") => {
    return (acao: string) => {
        const permissoes = {
            administrador: ["criar", "editar", "ler", "remover"],
            leitor: ["ler"],
        };
        return permissoes[cargo].includes(acao);
    };
};

const administrador = criarSistemaDePermissao("administrador");
const leitor = criarSistemaDePermissao("leitor");

const leitorPodeCriar = leitor("criar"); // false
const administradorPodeRemover = administrador("remover"); // true
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo criamos um cenário mais próximo do mundo real, como um sistema de permissões de uma aplicação com diferentes tipos de usuários.

Usamos a closure para criar uma função fábrica para administradores e leitores, assim criamos um sistema personalizado para cada tipo de usuário.

Como estamos usando uma closure, essa closure vai manter o estado, isso é, continua tendo acesso ao tipo de cargo definido quando a função foi executada pela primeira vez, assim, conseguimos usar a nossa closure para checar se o usuário que ela mantém armazenado possuí permissão para executar X ação.

Encapsulamento com closures

Assim como na orientação objetos, podemos usar closures para encapsular dados dentro de funções e restringir o acesso direto a eles, isso nos permite proteger o estado interno da nossa função e expor apenas o que for necessário utilizando funções com controle.

Veja o exemplo:

const meuCarrinhoDeCompras = () => {
    let itens: string[] = [];

    const adicionarItem = (item: string) => {
        itens.push(item);
    };

    const removerUltimoItem = () => {
        itens.pop();
    };

    const pegarItens = () => {
        return itens;
    };

    return { adicionarItem, removerUltimoItem, pegarItens };
};

const carrinho1 = meuCarrinhoDeCompras();

carrinho1.adicionarItem("maçã"); // [maçã]
carrinho1.adicionarItem("pera"); // [maçã, pera]
carrinho1.pegarItens(); // [maça, pera]
carrinho1.removerUltimoItem(); // [maçã]
Enter fullscreen mode Exit fullscreen mode

Criamos uma closure para manter o acesso aos dados de um carrinho de compras. Isso nos permite proteger a variável interna itens, impedindo que ela seja acessada ou modificada diretamente

Quando usar uma closure?

Espero que os exemplos que dei tenham ajudado a entender as utilidades das closures. Elas podem ser muito úteis em várias situações do dia a dia, como:

  • Manter o estado entre chamadas de uma função.
  • Encapsular dados para restringir acessos/modificações não controladas.
  • Manter o contexto de uma função, mesmo após ter sido executada.
  • Criar funções fábrica, como no exemplo do sistema de permissões, que cria configurações distintas para administrador e leitor.

Cuidado com a memória

Closures mantêm em memória todas as variáveis do escopo externo, mesmo quando não estão sendo usadas. Por isso, evite manter dados desnecessários dentro delas. Quando precisar, limpe referências que não são mais necessárias para evitar consumo excessivo de memória.

Top comments (0)