Voltar
Closures
Entendendo Closures: a mochila das funções
Vamos dar uma olhada nesse trecho de código e mergulhar de verdade em como o JavaScript lida com isso, especialmente os conceitos super maneiros de escopo e closures.
O Exemplo Principal
function outer() {
let counter = 0;
function incrementCounter() {
counter++;
}
return incrementCounter;
}
const anotherFunction = outer();
anotherFunction();
anotherFunction();
Mergulhando na Mente do JavaScript
Passando pelo nosso código e entendendo, a fundo, como o JavaScript lida com isso, podemos ir linha por linha e debugar o que está rolando.
Primeiro, DECLARAMOS uma função chamada outer
na nossa memória global (ou, mais precisamente, no ambiente de variáveis do contexto de execução global).
Depois, criamos uma variável anotherFunction
que recebe a invocação de outer
. Quando chamamos outer()
, criamos um novo contexto de execução e colocamos outer
na call stack.
Dentro do Mundo da Função outer
Agora estamos dentro do contexto de execução da função outer
! É aqui que a mágica começa:
-
Criamos uma variável
counter
com valor inicial 0. Essecounter
vive na memória local daouter
(que chamamos de variable environment ou lexical enviroment). -
DECLARAMOS a função
incrementCounter
. Aqui está um detalhe crucial: quandoincrementCounter
é declarada, ela “lembra” do ambiente que a cerca – aquele ondecounter
vive! Essa “memória” é o começo de um closure. -
Retornamos essa função
incrementCounter
. Quando fazemos isso, não estamos só mandando o código de volta; ela também está levando uma “mochila” – o lexical environment da funçãoouter()
(onde nossa variávelcounter
está!) – e anexando àincrementCounter
. Então,anotherFunction
agora tem o código daincrementCounter
mais essa mochila especial.
Depois do return, o contexto de execução da outer
é deletado, e a função outer()
sai da call stack. Mas aqui está o pulo do gato: como anotherFunction
ainda está segurando essa “mochila” (o closure), a variável counter
não desaparece! Ela fica por aí, privadamente ligada à anotherFunction
.
A Parte Maneira: Closures !
Agora vem a parte realmente divertida, então vamos bem devagar aqui!
A próxima linha de código chama anotherFunction()
. Isso cria um novo contexto de execução com sua própria memória local (ou variable environment) e empurra essa nova função pra call stack.
Dentro desse novo contexto de execução, executamos counter++
. Mas peraí, a função incrementCounter
não tem uma variável counter
diretamente no seu próprio escopo local!
Vamos procurar por ela, né? Começamos procurando na memória local do contexto de execução da função incrementCounter
. Encontramos counter
lá? Não!
Bom, se não encontramos aqui, qual é o próximo lugar que vamos procurar? A memória global, certo? Mas opa, também não encontramos no escopo global! Onde que a variável counter
está armazenada?!
É exatamente por isso que essa “mochila” é tão vital! Quando retornamos a função incrementCounter
, ela não só salvou seu código; ela também pegou o lexical environment da função outer()
(onde counter
vive) e colocou nas suas “costas”, funcionando como aquela mochila que falamos.
Então, sempre que chamamos anotherFunction()
, e incrementCounter
não encontra counter
na sua memória local imediata, ela não vai direto pra memória GLOBAL. Em vez disso, ela olha naquela “mochila” – seu closure – pra encontrar counter
do seu escopo externo.
Sempre que continuamos chamando anotherFunction()
, temos dados de contador permanentes e “privados”. Cada execução incrementa aquele contador específico, graças ao closure!
Exemplo Prático: Adicionar por X
function addByX(x) {
function addNumberByX(number) {
return number + x;
}
return addNumberByX;
}
// /*** Descomente essas linhas pra testar! ***/
const addByTwo = addByX(2);
//console.log(addByTwo(1)) // => deve retornar 3
//console.log(addByTwo(2)); // => deve retornar 4
//console.log(addByTwo(3)) // => deve retornar 5
const addByThree = addByX(3);
//console.log(addByThree(1)) // => deve retornar 4
//console.log(addByThree(2)) // => deve retornar 5
const addByFour = addByX(4);
//console.log(addByFour(4)) // => deve retornar 8
//console.log(addByFour(5)) // => deve retornar 9
Aplicações Avançadas
Memoização
function saveOutput(func, magicWord) {
let resultObjects = {};
return function output(argumentNumOrString) {
if (Number.parseInt(argumentNumOrString)) {
const resultFromArgumentFunction = func(argumentNumOrString);
resultObjects[argumentNumOrString] = resultFromArgumentFunction;
return resultFromArgumentFunction;
} else if (argumentNumOrString === magicWord) {
return resultObjects;
}
};
}
Debouncing e Throttling
function debounce(callback, interval) {
let counter = 0;
let hasRan = false;
function closureFn() {
let id = undefined;
if (!hasRan) {
id = setInterval(() => counter++, 1);
hasRan = true;
return callback();
} else {
if (counter < interval) {
counter = 0;
clearInterval(id);
id = setInterval(() => counter++, 1);
return undefined;
} else {
counter = 0;
clearInterval(id);
id = setInterval(() => counter++, 1);
return callback();
}
}
}
return closureFn;
}
// Função que permite a invocação do callback
// depois que interval milissegundos passaram desde a
// última vez que a função retornada rodou
Pontos-Chave
- Closures permitem que funções internas acessem variáveis do seu escopo externo mesmo depois que a função externa terminou de executar
- A metáfora da “mochila” ajuda a visualizar como closures preservam o lexical environment