Aguarde...

14 de maio de 2019

Eu nunca entendi o fechamento de JavaScript

Eu nunca entendi o fechamento de JavaScript

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 contextcomo 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:

  1. JavaScript cria um novo contexto de execução, um contexto de execução local
  2. 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.
  3. 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 returndeclaração ou encontra um colchete de fechamento }. Quando uma função termina, acontece o seguinte:

  1. Os contextos de execução local saem da pilha de execução
  2. 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 returndeclaração, undefinedserá retornada.
  3. 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.

  1. Na linha 1, declaramos uma nova variável ano contexto de execução global e atribuímos o número 3.
  2. Em seguida, fica complicado. As linhas 2 a 5 estão realmente juntas. o que acontece aqui? Declaramos uma nova variável nomeada addTwono 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 a addTwo. O código dentro da função não é avaliado, não executado, apenas armazenado em uma variável para uso futuro.
  3. 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 de undefined.
  4. 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 ou undefined). O que for retornado da função será atribuído à variável b.
  5. 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 denominada addTwo. Oh, encontrou um, foi definido no passo 2 (ou linhas 2-5). E eis e eis que a variável addTwocontém uma definição de função. Note que a variável aé passada como um argumento para a função. JavaScript procura por uma variável aem sua memória de contexto de execução global , encontra, descobre que seu valor é 3e passa o número 3como um argumento para a função. Pronto para executar a função.
  6. 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?
  7. 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ável xé declarada no contexto de execução local. E como o valor 3foi passado como argumento, a variável x recebe o número 3.
  8. O próximo passo é: Uma nova variável reté declarada no contexto de execução local . Seu valor é definido como indefinido. (linha 3)
  9. Ainda linha 3, uma adição precisa ser executada. Primeiro precisamos do valor de x. JavaScript irá procurar por uma variável x. Ele procurará no contexto de execução local primeiro. E encontrou um, o valor é 3. E o segundo operando é o número 2. O resultado da adição ( 5) é atribuído à variável ret.
  10. Linha 4. Retornamos o conteúdo da variável ret. Outra consulta no contexto de execução local . retcontém o valor 5. A função retorna o número 5. E a função termina.
  11. Linhas 4-5. A função termina. O contexto de execução local é destruído. As variáveis xretsã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ção addTwofoi chamada a partir do contexto de execução global.
  12. Agora retomamos de onde paramos na etapa 4. O valor retornado (número 5) é atribuído à variável b. Ainda estamos na linha 6 do pequeno programa.
  13. 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úmero 5.

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.

  1. Declare uma nova variável val1 no contexto de execução global e atribua a ela o número 2.
  2. Linhas 2-5. Declare uma nova variável multiplyThis e atribua a ela uma definição de função.
  3. Linha 6. Declare uma nova variável multiplied no contexto de execução global.
  4. Recupere a variável multiplyThis da memória de contexto de execução global e execute-a como uma função. Passe o número como argumento.
  5. Nova chamada de função = novo contexto de execução. Crie um novo contexto de execução local.
  6. No contexto de execução local, declare uma variável e atribua o número 6.
  7. Linha 3. No contexto de execução local, declare uma variável ret.
  8. Linha 3 (continuação). Execute uma multiplicação com dois operandos; o conteúdo das variáveis nval1. Procure a variável nno contexto de execução local. Nós o declaramos na etapa 6. Seu conteúdo é o número 6. Procure a variável val1no contexto de execução local. O contexto de execução local não possui uma variável rotulada val1. Vamos verificar o contexto de chamada. O contexto de chamada é o contexto de execução global. Vamos procurar val1no contexto de execução global. Oh sim, está lá. Foi definido no passo 1. O valor é o número 2.
  9. Linha 3 (continuação). Multiplique os dois operandos e atribua-o à retvariável. 6 * 2 = 12. reté agora 12.
  10. Retorna a retvariável. O contexto de execução local é destruído, junto com suas variáveis retn. A variável val1 não é destruída, pois fazia parte do contexto de execução global.
  11. Regressar à linha 6. No contexto de chamada, o número 12é atribuído à multiplied variável.
  12. Finalmente, na linha 7, mostramos o valor da multipliedvariá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 addTworetorna 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.

  1. Linha 1. Declaramos uma variável valno contexto de execução global e atribuímos o número 7a essa variável.
  2. Linhas 2-8. Declaramos uma variável nomeada createAdderno 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).
  3. Linha 9. Declaramos uma nova variável, denominada adder, no contexto de execução global. Temporariamente, undefinedé atribuído a adder.
  4. 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 chamada createAdder. Foi criado no passo 2. Ok, vamos chamá-lo.
  5. 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.
  6. Ainda linhas 3-6. Nós temos uma nova declaração de função. Criamos uma variável addNumbersno contexto de execução local. Isso é importante. addNumbersexiste apenas no contexto de execução local. Nós armazenamos uma definição de função na variável local nomeada addNumbers.
  7. Agora estamos na linha 7. Retornamos o conteúdo da variável addNumbers. O mecanismo procura por uma variável nomeada addNumberse 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 de addNumbers. 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.
  8. Após return, o contexto de execução local é destruído. A addNumbersvariável não é mais. A definição da função ainda existe, porém, é retornada da função e é atribuída à variável adder; essa é a variável que criamos na etapa 3.
  9. Agora estamos na linha 10. Definimos uma nova variável sumno contexto de execução global. Atribuição temporária é undefined.
  10. 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.
  11. 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úmero 7e o segundo é o número 8.
  12. 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: ab. Eles recebem respectivamente os valores 78, como esses foram os argumentos que passamos para a função na etapa anterior.
  13. Linha 4. Uma nova variável é declarada, nomeada ret. É declarado no contexto de execução local.
  14. Linha 4. Uma adição é executada, onde adicionamos o conteúdo da variável ae o conteúdo da variável b. O resultado da adição ( 15) é atribuído à retvariável.
  15. retvariável é retornada dessa função. O contexto de execução local será destruída, ele é removido da pilha de chamadas, as variáveis abretnão existem mais.
  16. O valor retornado é atribuído à sumvariável que definimos na etapa 9.
  17. Nós imprimimos o valor sumpara 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 returnou o colchete de fechamento }.

Finalmente, um fechamento

Dê uma olhada no próximo código e tente descobrir o que vai acontecer.

1: function createCounter() { 
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.

  1. Linhas 1 a 8 Criamos uma nova variável createCounterno contexto de execução global e definimos a função atribuída a get.
  2. Linha 9. Declaramos uma nova variável nomeada incrementno contexto de execução global.
  3. Linha 9 novamente. Precisamos chamar a createCounterfunção e atribuir seu valor retornado à incrementvariável.
  4. Linhas 1 a 8 Chamando a função. Criando novo contexto de execução local.
  5. Linha 2. Dentro do contexto de execução local, declare uma nova variável denominada counter. Number 0é atribuído a counter.
  6. 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.
  7. Linha 7. Retornando o conteúdo da myFunctionvariável. Contexto de execução local é excluído. myFunctioncounternão existem mais. O controle é retornado ao contexto de chamada.
  8. Linha 9. No contexto de chamada, o contexto de execução global, o valor retornado por createCounteré atribuído a increment. O incremento da variável agora contém uma definição de função. A definição da função que foi retornada por createCounter. Não é mais rotulado myFunction, mas é a mesma definição. Dentro do contexto global, é rotulado increment.
  9. Linha 10. Declarar uma nova variável ( c1).
  10. 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.
  11. Crie um novo contexto de execução. Não há parâmetros. Inicie a execução da função.
  12. Linha 4 counter = counter + 1. Procure o valor counterno 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 rotulada counteraqui. O Javascript irá avaliar isso como counter = undefined + 1, declarar uma nova variável local rotulada countere atribuir-lhe o número 1, como undefinedé mais ou menos 0.
  13. Linha 5. Retornamos o conteúdo counterou o número 1. Nós destruímos o contexto de execução local e a countervariável.
  14. Voltar para a linha 10. O valor retornado ( 1) é atribuído a c1.
  15. Linha 11. Repetimos os passos 10-14, também c2é atribuído 1.
  16. Linha 12. Repetimos os passos 10-14, também c3é atribuído 1.
  17. Linha 13. Nós registramos o conteúdo de variáveis c1c2c3.

Tente isso por si mesmo e veja o que acontece. Você vai notar que não está registrando 111como você pode esperar de minha explicação acima. Em vez disso, está registrando 123. 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 incrementconté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: function createCounter() { 
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)
  1. Linhas 1 a 8 Criamos uma nova variável createCounterno contexto de execução global e definimos a função atribuída a get. O mesmo que acima.
  2. Linha 9. Declaramos uma nova variável nomeada incrementno contexto de execução global. O mesmo que acima.
  3. Linha 9 novamente. Precisamos chamar a createCounterfunção e atribuir seu valor retornado à incrementvariável. O mesmo que acima.
  4. Linhas 1 a 8 Chamando a função. Criando novo contexto de execução local. O mesmo que acima.
  5. Linha 2. Dentro do contexto de execução local, declare uma nova variável denominada counter. Number 0é atribuído a counter. O mesmo que acima.
  6. 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ável counter(com o valor de 0).
  7. Linha 7. Retornando o conteúdo da myFunctionvariável. Contexto de execução local é excluído. myFunctioncounternã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.
  8. Linha 9. No contexto de chamada, o contexto de execução global, o valor retornado por createCounteré atribuído a increment. O incremento variável agora contém uma definição de função (e fechamento). A definição da função que foi retornada por createCounter. Não é mais rotulado myFunction, mas é a mesma definição. Dentro do contexto global, é chamado increment.
  9. Linha 10. Declarar uma nova variável ( c1).
  10. 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)
  11. Crie um novo contexto de execução. Não há parâmetros. Inicie a execução da função.
  12. Linha 4 counter = counter + 1. Precisamos procurar a variável counter. 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 chamada counter, seu valor é 0. Após a expressão na linha 4, seu valor é definido como 1. E é armazenado na mochila novamente. O fechamento agora contém a variável countercom um valor de 1.
  13. Linha 5. Retornamos o conteúdo counterou o número 1. Nós destruímos o contexto de execução local.
  14. Voltar para a linha 10. O valor retornado ( 1) é atribuído a c1.
  15. Linha 11. Repetimos os passos 10-14. Desta vez, quando olhamos para o nosso fechamento, vemos que a countervariável tem um valor de 1. Foi definido no passo 12 ou na linha 4 do programa. Seu valor é incrementado e armazenado como 2no fechamento da função de incremento. E c2é designado 2.
  16. Linha 12. Repetimos as etapas 10 a 14, c3atribuídas 3.
  17. Linha 13. Nós registramos o conteúdo de variáveis c1c2c3.

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 addXque 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 xfaz 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 xde seu fechamento e à variável nque foi passada como um argumento e é capaz de retornar a soma.

Neste exemplo, o console imprimirá o número 7.

Postado em BlogTags:
Escreva um comentário