Até que alguém me explicou assim…
Como o título afirma, o fechamento de JavaScript sempre foi um mistério para mim. Eu li vários artigos , usei encerramentos em meu trabalho, às vezes até usei um fechamento sem perceber que estava usando um fechamento.
Recentemente eu fui a uma palestra onde alguém realmente explicou de uma forma que finalmente clicou para mim. Tentarei usar essa abordagem para explicar os encerramentos deste artigo. Deixe-me dar crédito ao grande pessoal da CodeSmith e sua série JavaScript The Hard Parts .
Antes de começarmos
Alguns conceitos são importantes para o grok antes que você possa grok closures. Um deles é o contexto de execução .
Este artigo tem uma boa introdução ao contexto de execução. Para citar o artigo:Quando o código é executado em JavaScript, o ambiente no qual ele é executado é muito importante e é avaliado como um dos seguintes:
Código global - O ambiente padrão no qual seu código é executado pela primeira vez.
Código de função - Sempre que o fluxo de execução entra em um corpo da função.(…)(…), Vamos pensar no termo
execution context
como o ambiente / escopo no qual o código atual está sendo avaliado.
Em outras palavras, quando começamos o programa, começamos no contexto de execução global. Algumas variáveis são declaradas no contexto de execução global. Nós chamamos essas variáveis globais. Quando o programa chama uma função, o que acontece? Alguns passos:
- JavaScript cria um novo contexto de execução, um contexto de execução local
- Esse contexto de execução local terá seu próprio conjunto de variáveis, essas variáveis serão locais para esse contexto de execução.
- O novo contexto de execução é lançado na pilha de execução . Pense na pilha de execução como um mecanismo para rastrear onde o programa está em sua execução.
Quando a função termina? Quando encontra uma return
declaração ou encontra um colchete de fechamento }
. Quando uma função termina, acontece o seguinte:
- Os contextos de execução local saem da pilha de execução
- As funções enviam o valor de retorno de volta ao contexto de chamada. O contexto de chamada é o contexto de execução que chamou essa função, pode ser o contexto de execução global ou outro contexto de execução local. Cabe ao contexto de execução de chamada lidar com o valor de retorno nesse ponto. O valor retornado poderia ser um objeto, uma matriz, uma função, um booleano, qualquer coisa realmente. Se a função não tiver
return
declaração,undefined
será retornada. - O contexto de execução local é destruído. Isso é importante. Destruído. Todas as variáveis que foram declaradas no contexto de execução local são apagadas. Eles não estão mais disponíveis. É por isso que eles são chamados de variáveis locais.
Um exemplo muito básico
Antes de fecharmos, vamos dar uma olhada no seguinte trecho de código. Parece muito simples, quem lê este artigo provavelmente sabe exatamente o que faz.
1: deixa a = 3
2: function addTwo (x) {
3: deixa ret = x + 2
4: retorna ret
5:}
6: deixa b = addTwo (a)
7: console.log (b)
Para entender como o mecanismo JavaScript realmente funciona, vamos detalhar isso em detalhes.
- Na linha 1, declaramos uma nova variável
a
no contexto de execução global e atribuímos o número3
. - Em seguida, fica complicado. As linhas 2 a 5 estão realmente juntas. o que acontece aqui? Declaramos uma nova variável nomeada
addTwo
no contexto de execução global. E o que nós atribuímos a isso? Uma definição de função. O que quer que esteja entre os dois suportes{ }
é atribuído aaddTwo
. O código dentro da função não é avaliado, não executado, apenas armazenado em uma variável para uso futuro. - Então agora estamos na linha 6. Parece simples, mas há muito para descompactar aqui. Primeiro, declaramos uma nova variável no contexto de execução global e a rotulamos
b
. Assim que uma variável é declarada, ela tem o valor deundefined
. - Em seguida, ainda na linha 6, vemos um operador de atribuição. Estamos nos preparando para atribuir um novo valor à variável
b
. Em seguida, vemos uma função sendo chamada. Quando você vê uma variável seguida de parênteses(…)
, esse é o sinal de que uma função está sendo chamada. Flash forward, cada função retorna algo (um valor, um objeto ouundefined
). O que for retornado da função será atribuído à variávelb
. - Mas primeiro precisamos chamar a função rotulada
addTwo
. O JavaScript irá procurar em sua memória de contexto de execução global uma variável denominadaaddTwo
. Oh, encontrou um, foi definido no passo 2 (ou linhas 2-5). E eis e eis que a variáveladdTwo
contém uma definição de função. Note que a variávela
é passada como um argumento para a função. JavaScript procura por uma variávela
em sua memória de contexto de execução global , encontra, descobre que seu valor é3
e passa o número3
como um argumento para a função. Pronto para executar a função. - Agora o contexto de execução irá mudar. Um novo contexto de execução local é criado, vamos chamar de ‘addTwo execution context’. O contexto de execução é empurrado para a pilha de chamadas. Qual é a primeira coisa que fazemos no contexto de execução local?
- Você pode ser tentado a dizer: “Uma nova variável
ret
é declarada no contexto de execução local ”. Essa não é a resposta. A resposta correta é que precisamos examinar primeiro os parâmetros da função. Uma nova variávelx
é declarada no contexto de execução local. E como o valor3
foi passado como argumento, a variável x recebe o número3
. - O próximo passo é: Uma nova variável
ret
é declarada no contexto de execução local . Seu valor é definido como indefinido. (linha 3) - Ainda linha 3, uma adição precisa ser executada. Primeiro precisamos do valor de
x
. JavaScript irá procurar por uma variávelx
. Ele procurará no contexto de execução local primeiro. E encontrou um, o valor é3
. E o segundo operando é o número2
. O resultado da adição (5
) é atribuído à variávelret
. - Linha 4. Retornamos o conteúdo da variável
ret
. Outra consulta no contexto de execução local .ret
contém o valor5
. A função retorna o número5
. E a função termina. - Linhas 4-5. A função termina. O contexto de execução local é destruído. As variáveis
x
eret
são eliminadas. Eles não existem mais. O contexto é exibido da pilha de chamadas e o valor de retorno é retornado ao contexto de chamada. Nesse caso, o contexto de chamada é o contexto de execução global, porque a funçãoaddTwo
foi chamada a partir do contexto de execução global. - Agora retomamos de onde paramos na etapa 4. O valor retornado (número
5
) é atribuído à variávelb
. Ainda estamos na linha 6 do pequeno programa. - Eu não estou entrando em detalhes, mas na linha 7, o conteúdo da variável
b
é impresso no console. Em nosso exemplo, o número5
.
Essa foi uma explicação muito longa para um programa muito simples, e nós ainda não tocamos em encerramentos. Nós chegaremos lá, eu prometo. Mas primeiro precisamos fazer outro desvio ou dois.
Escopo léxico.
Precisamos entender alguns aspectos do escopo léxico. Dê uma olhada no exemplo a seguir.
1: deixe val1 = 2
2: function multipliqueEste (n) {
3: deixe ret = n * val1
4: retorne ret
5:}
6: deixe multiplicado = multipliqueEste (6)
7: console.log ('exemplo de escopo:' multiplicado
A ideia aqui é que temos variáveis no contexto de execução local e variáveis no contexto de execução global. Uma intrincada do JavaScript é como ele procura por variáveis. Se não puder encontrar uma variável em seu contexto de execução local , ela procurará em seu contexto de chamada. E se não for encontrado lá em seu contexto de chamada. Repetidamente, até que esteja procurando no contexto de execução global . (E se não encontrar, é undefined
). Siga junto com o exemplo acima, isso irá esclarecê-lo. Se você entender como o escopo funciona, você pode pular isso.
- Declare uma nova variável
val1
no contexto de execução global e atribua a ela o número2
. - Linhas 2-5. Declare uma nova variável
multiplyThis
e atribua a ela uma definição de função. - Linha 6. Declare uma nova variável
multiplied
no contexto de execução global. - Recupere a variável
multiplyThis
da memória de contexto de execução global e execute-a como uma função. Passe o número6
como argumento. - Nova chamada de função = novo contexto de execução. Crie um novo contexto de execução local.
- No contexto de execução local, declare uma variável
n
e atribua o número 6. - Linha 3. No contexto de execução local, declare uma variável
ret
. - Linha 3 (continuação). Execute uma multiplicação com dois operandos; o conteúdo das variáveis
n
eval1
. Procure a variáveln
no contexto de execução local. Nós o declaramos na etapa 6. Seu conteúdo é o número6
. Procure a variávelval1
no contexto de execução local. O contexto de execução local não possui uma variável rotuladaval1
. Vamos verificar o contexto de chamada. O contexto de chamada é o contexto de execução global. Vamos procurarval1
no contexto de execução global. Oh sim, está lá. Foi definido no passo 1. O valor é o número2
. - Linha 3 (continuação). Multiplique os dois operandos e atribua-o à
ret
variável. 6 * 2 = 12.ret
é agora12
. - Retorna a
ret
variável. O contexto de execução local é destruído, junto com suas variáveisret
en
. A variávelval1
não é destruída, pois fazia parte do contexto de execução global. - Regressar à linha 6. No contexto de chamada, o número
12
é atribuído àmultiplied
variável. - Finalmente, na linha 7, mostramos o valor da
multiplied
variável no console.
Portanto, neste exemplo, precisamos lembrar que uma função tem acesso a variáveis definidas em seu contexto de chamada. O nome formal deste fenômeno é o escopo léxico.
Uma função que retorna uma função
No primeiro exemplo, a função addTwo
retorna um número. Lembre-se de que uma função pode retornar alguma coisa. Vejamos um exemplo de uma função que retorna uma função, pois isso é essencial para entender os encerramentos. Aqui está o exemplo que vamos analisar.
1: let val = 7
2: function createAdder () {
3: função addNumbers (a, b) {
4: ret ret = a + b
5: retorno ret
6:}
7: return addNumbers
8:}
9: deixa adder = createAdder ()
10: let soma = adder (val, 8)
11: console.log ('exemplo de função retornando uma função:', soma)
Vamos voltar ao colapso passo a passo.
- Linha 1. Declaramos uma variável
val
no contexto de execução global e atribuímos o número7
a essa variável. - Linhas 2-8. Declaramos uma variável nomeada
createAdder
no contexto de execução global e atribuímos uma definição de função a ela. As linhas 3 a 7 descrevem a dita definição de função. Como antes, neste ponto, não estamos entrando nessa função. Nós apenas armazenamos a definição da função nessa variável (createAdder
). - Linha 9. Declaramos uma nova variável, denominada
adder
, no contexto de execução global. Temporariamente,undefined
é atribuído aadder
. - Ainda linha 9. Nós vemos os parênteses
()
; precisamos executar ou chamar uma função. Vamos consultar a memória do contexto de execução global e procurar uma variável chamadacreateAdder
. Foi criado no passo 2. Ok, vamos chamá-lo. - Chamando uma função. Agora estamos na linha 2. Um novo contexto de execução local é criado. Podemos criar variáveis locais no novo contexto de execução. O mecanismo adiciona o novo contexto à pilha de chamadas. A função não tem argumentos, vamos pular direto para o corpo dela.
- Ainda linhas 3-6. Nós temos uma nova declaração de função. Criamos uma variável
addNumbers
no contexto de execução local. Isso é importante.addNumbers
existe apenas no contexto de execução local. Nós armazenamos uma definição de função na variável local nomeadaaddNumbers
. - Agora estamos na linha 7. Retornamos o conteúdo da variável
addNumbers
. O mecanismo procura por uma variável nomeadaaddNumbers
e a encontra. É uma definição de função. Tudo bem, uma função pode retornar qualquer coisa, incluindo uma definição de função. Então, nós retornamos a definição deaddNumbers
. Qualquer coisa entre os parênteses nas linhas 4 e 5 compõe a definição da função. Também removemos o contexto de execução local da pilha de chamadas. - Após
return
, o contexto de execução local é destruído. AaddNumbers
variável não é mais. A definição da função ainda existe, porém, é retornada da função e é atribuída à variáveladder
; essa é a variável que criamos na etapa 3. - Agora estamos na linha 10. Definimos uma nova variável
sum
no contexto de execução global. Atribuição temporária éundefined
. - Precisamos executar uma função a seguir. Qual função? A função que é definida na variável denominada
adder
. Procuramos isso no contexto de execução global, e com certeza o encontramos. É uma função que leva dois parâmetros. - Vamos recuperar os dois parâmetros, para que possamos chamar a função e passar os argumentos corretos. O primeiro é a variável
val
, que definimos no passo 1, representa o número7
e o segundo é o número8
. - Agora temos que executar essa função. A definição da função é linhas delineadas de 3 a 5. Um novo contexto de execução local é criado. Dentro do contexto local, duas novas variáveis são criadas:
a
eb
. Eles recebem respectivamente os valores7
e8
, como esses foram os argumentos que passamos para a função na etapa anterior. - Linha 4. Uma nova variável é declarada, nomeada
ret
. É declarado no contexto de execução local. - Linha 4. Uma adição é executada, onde adicionamos o conteúdo da variável
a
e o conteúdo da variávelb
. O resultado da adição (15
) é atribuído àret
variável. - A
ret
variável é retornada dessa função. O contexto de execução local será destruída, ele é removido da pilha de chamadas, as variáveisa
,b
eret
não existem mais. - O valor retornado é atribuído à
sum
variável que definimos na etapa 9. - Nós imprimimos o valor
sum
para o console.
Como esperado, o console irá imprimir 15. Nós realmente passamos por um monte de aros aqui. Estou tentando ilustrar alguns pontos aqui. Primeiro, uma definição de função pode ser armazenada em uma variável, a definição da função é invisível para o programa até ser chamada. Segundo, toda vez que uma função é chamada, um contexto de execução local é (temporariamente) criado. Esse contexto de execução desaparece quando a função é executada. Uma função é feita quando encontra return
ou o colchete de fechamento }
.
Finalmente, um fechamento
Dê uma olhada no próximo código e tente descobrir o que vai acontecer.
1: functioncreateCounter
() {
2: let contador = 0
3: const myFunction = function () {
4: contador = contador + 1
5: retorno contador
6:}
7: return myFunction
8:}
9: const incremento = createCounter ()
10: const c1 = incremento ()
11: const c2 = incremento ()
12: const c3 = incremento ()
13: console.log ('incremento de exemplo', c1, c2, c3)
Agora que pegamos o jeito dos dois exemplos anteriores, vamos passar pela execução disso, como esperamos que seja executado.
- Linhas 1 a 8 Criamos uma nova variável
createCounter
no contexto de execução global e definimos a função atribuída a get. - Linha 9. Declaramos uma nova variável nomeada
increment
no contexto de execução global. - Linha 9 novamente. Precisamos chamar a
createCounter
função e atribuir seu valor retornado àincrement
variável. - Linhas 1 a 8 Chamando a função. Criando novo contexto de execução local.
- Linha 2. Dentro do contexto de execução local, declare uma nova variável denominada
counter
. Number0
é atribuído acounter
. - Linha 3–6. Declarando nova variável nomeada
myFunction
. A variável é declarada no contexto de execução local. O conteúdo da variável é ainda outra definição de função. Conforme definido nas linhas 4 e 5. - Linha 7. Retornando o conteúdo da
myFunction
variável. Contexto de execução local é excluído.myFunction
ecounter
não existem mais. O controle é retornado ao contexto de chamada. - Linha 9. No contexto de chamada, o contexto de execução global, o valor retornado por
createCounter
é atribuído aincrement
. O incremento da variável agora contém uma definição de função. A definição da função que foi retornada porcreateCounter
. Não é mais rotuladomyFunction
, mas é a mesma definição. Dentro do contexto global, é rotuladoincrement
. - Linha 10. Declarar uma nova variável (
c1
). - Linha 10 (continuação). Procure a variável
increment
, é uma função, chame. Ele contém a definição de função retornada anteriormente, conforme definido nas linhas 4–5. - Crie um novo contexto de execução. Não há parâmetros. Inicie a execução da função.
- Linha 4
counter = counter + 1
. Procure o valorcounter
no contexto de execução local. Acabamos de criar esse contexto e nunca declarar quaisquer variáveis locais. Vamos olhar no contexto de execução global. Nenhuma variável rotuladacounter
aqui. O Javascript irá avaliar isso comocounter = undefined + 1
, declarar uma nova variável local rotuladacounter
e atribuir-lhe o número1
, comoundefined
é mais ou menos0
. - Linha 5. Retornamos o conteúdo
counter
ou o número1
. Nós destruímos o contexto de execução local e acounter
variável. - Voltar para a linha 10. O valor retornado (
1
) é atribuído ac1
. - Linha 11. Repetimos os passos 10-14, também
c2
é atribuído1
. - Linha 12. Repetimos os passos 10-14, também
c3
é atribuído1
. - Linha 13. Nós registramos o conteúdo de variáveis
c1
,c2
ec3
.
Tente isso por si mesmo e veja o que acontece. Você vai notar que não está registrando 1
, 1
e 1
como você pode esperar de minha explicação acima. Em vez disso, está registrando 1
, 2
e 3
. Então, o que dá?
De alguma forma, a função de incremento lembra esse counter
valor. Como isso funciona?
Faz counter
parte do contexto de execução global? Tente console.log(counter)
e você vai conseguir undefined
. Então não é isso.
Talvez, quando você liga increment
, de alguma forma ele volta para a função onde foi criado ( createCounter
)? Como isso funcionaria? A variável increment
contém a definição da função, não de onde ela veio. Então não é isso.
Então deve haver outro mecanismo. O encerramento. Nós finalmente chegamos a ele, a peça que faltava.
Aqui está como isso funciona. Sempre que você declara uma nova função e a atribui a uma variável, armazena a definição da função, bem como um fechamento . O encerramento contém todas as variáveis que estão no escopo no momento da criação da função. É análogo a uma mochila. Uma definição de função vem com uma pequena mochila. E no seu pacote armazena todas as variáveis que estavam no escopo no momento em que a definição da função foi criada.
Então, nossa explicação acima estava toda errada , vamos tentar de novo, mas desta vez corretamente.
1: functioncreateCounter
() {
2: let contador = 0
3: const myFunction = function () {
4: contador = contador + 1
5: retorno contador
6:}
7: return myFunction
8:}
9: const incremento = createCounter ()
10: const c1 = incremento ()
11: const c2 = incremento ()
12: const c3 = incremento ()
13: console.log ('incremento de exemplo', c1, c2, c3)
- Linhas 1 a 8 Criamos uma nova variável
createCounter
no contexto de execução global e definimos a função atribuída a get. O mesmo que acima. - Linha 9. Declaramos uma nova variável nomeada
increment
no contexto de execução global. O mesmo que acima. - Linha 9 novamente. Precisamos chamar a
createCounter
função e atribuir seu valor retornado àincrement
variável. O mesmo que acima. - Linhas 1 a 8 Chamando a função. Criando novo contexto de execução local. O mesmo que acima.
- Linha 2. Dentro do contexto de execução local, declare uma nova variável denominada
counter
. Number0
é atribuído acounter
. O mesmo que acima. - Linha 3–6. Declarando nova variável nomeada
myFunction
. A variável é declarada no contexto de execução local. O conteúdo da variável é ainda outra definição de função. Conforme definido nas linhas 4 e 5. Agora também criamos um fechamento e o incluímos como parte da definição da função. O encerramento contém as variáveis que estão no escopo, neste caso, a variávelcounter
(com o valor de0
). - Linha 7. Retornando o conteúdo da
myFunction
variável. Contexto de execução local é excluído.myFunction
ecounter
não existem mais. O controle é retornado ao contexto de chamada. Então, estamos retornando a definição de função e seu fechamento , a mochila com as variáveis que estavam no escopo quando ela foi criada. - Linha 9. No contexto de chamada, o contexto de execução global, o valor retornado por
createCounter
é atribuído aincrement
. O incremento variável agora contém uma definição de função (e fechamento). A definição da função que foi retornada porcreateCounter
. Não é mais rotuladomyFunction
, mas é a mesma definição. Dentro do contexto global, é chamadoincrement
. - Linha 10. Declarar uma nova variável (
c1
). - Linha 10 (continuação). Procure a variável
increment
, é uma função, chame. Ele contém a definição de função retornada anteriormente, conforme definido nas linhas 4–5. (e também tem uma mochila com variáveis) - Crie um novo contexto de execução. Não há parâmetros. Inicie a execução da função.
- Linha 4
counter = counter + 1
. Precisamos procurar a variávelcounter
. Antes de olharmos no contexto de execução local ou global , vamos examinar nossa mochila. Vamos verificar o fechamento. Lo e eis que, o fechamento contém uma variável chamadacounter
, seu valor é0
. Após a expressão na linha 4, seu valor é definido como1
. E é armazenado na mochila novamente. O fechamento agora contém a variávelcounter
com um valor de1
. - Linha 5. Retornamos o conteúdo
counter
ou o número1
. Nós destruímos o contexto de execução local. - Voltar para a linha 10. O valor retornado (
1
) é atribuído ac1
. - Linha 11. Repetimos os passos 10-14. Desta vez, quando olhamos para o nosso fechamento, vemos que a
counter
variável tem um valor de 1. Foi definido no passo 12 ou na linha 4 do programa. Seu valor é incrementado e armazenado como2
no fechamento da função de incremento. Ec2
é designado2
. - Linha 12. Repetimos as etapas 10 a 14,
c3
atribuídas3
. - Linha 13. Nós registramos o conteúdo de variáveis
c1
,c2
ec3
.
Então agora entendemos como isso funciona. A chave para lembrar é que quando uma função é declarada, ela contém uma definição de função e um encerramento. O encerramento é uma coleção de todas as variáveis no escopo no momento da criação da função.
Você pode perguntar se alguma função possui um encerramento, mesmo funções criadas no escopo global? A resposta é sim. Funções criadas no escopo global criam um fechamento. Mas como essas funções foram criadas no escopo global, elas têm acesso a todas as variáveis no escopo global. E o conceito de fechamento não é realmente relevante.
Quando uma função retorna uma função, é quando o conceito de fechamento se torna mais relevante. A função retornada tem acesso a variáveis que não estão no escopo global, mas elas existem somente em seu encerramento.
Fechamentos não tão triviais
Às vezes, fechamentos aparecem quando você nem percebe isso. Você pode ter visto um exemplo do que chamamos de aplicação parcial. Como no código a seguir.
vamos c = 4
const addX = x => n => n + x
const addThree = addX (3)
vamos d = addThree (c)
console.log ('exemplo de aplicação parcial', d)
No caso de a função de seta lançar você, aqui está o equivalente.
let c = 4
função addX (x) {
função return (n) {
retorno n + x
}
}
const addThree = addX (3)
vamos d = addThree (c)
console.log ('exemplo de aplicação parcial', d)
Declaramos uma função de somador genérica addX
que recebe um parâmetro ( x
) e retorna outra função.
A função retornada também recebe um parâmetro e o adiciona à variável x
.
A variável x
faz parte do fechamento. Quando a variável addThree
é declarada no contexto local, é atribuída uma definição de função e um fechamento. O fechamento contém a variável x
.
Então, agora quando addThree
é chamado e executado, ele tem acesso à variável x
de seu fechamento e à variável n
que foi passada como um argumento e é capaz de retornar a soma.
Neste exemplo, o console imprimirá o número 7
.