Como funcionam os fechamentos de JavaScript?


14 Answers

Sempre que você vir a palavra-chave da função dentro de outra função, a função interna terá acesso a variáveis ​​na função externa.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Isto irá sempre logar 16, porque bar pode acessar o x que foi definido como um argumento para foo , e ele também pode acessar tmp de foo .

Isso é um encerramento. Uma função não precisa retornar para ser chamada de encerramento. Simplesmente acessar variáveis ​​fora do seu escopo léxico imediato cria um fechamento .

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2); // bar is now a closure.
bar(10);

A função acima também registrará 16, porque a bar ainda pode se referir a x e tmp , mesmo que não esteja mais diretamente dentro do escopo.

No entanto, como o tmp ainda está por dentro do fechamento da bar , ele também está sendo incrementado. Ele será incrementado toda vez que você ligar para a bar .

O exemplo mais simples de fechamento é este:

var a = 10;
function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Quando uma função JavaScript é invocada, um novo contexto de execução é criado. Juntamente com os argumentos de função e o objeto pai, esse contexto de execução também recebe todas as variáveis ​​declaradas fora dele (no exemplo acima, tanto 'a' quanto 'b').

É possível criar mais de uma função de fechamento, retornando uma lista delas ou configurando-as para variáveis ​​globais. Todos estes se referem ao mesmo x ao mesmo tmp , eles não fazem suas próprias cópias.

Aqui o número x é um número literal. Como com outros literais em JavaScript, quando foo é chamado, o número x é copiado em foo como seu argumento x .

Por outro lado, JavaScript sempre usa referências ao lidar com objetos. Se disser, você chamou foo com um objeto, o fechamento retornará fará referência ao objeto original!

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    console.log(x.memb);
  }
}

var age = new Number(2);
var bar = foo(age); // bar is now a closure referencing age.
bar(10);

Como esperado, cada chamada para bar(10) irá incrementar x.memb . O que pode não ser esperado, é que x está simplesmente se referindo ao mesmo objeto que a variável age ! Depois de um par de chamadas para bar , age.memb será 2! Essa referência é a base para vazamentos de memória com objetos HTML.

Question

Como você explicaria os encerramentos de JavaScript para alguém com conhecimento dos conceitos que eles consistem (por exemplo, funções, variáveis ​​e afins), mas não entende os encerramentos em si?

Eu vi o exemplo Scheme dado na Wikipedia, mas infelizmente isso não ajudou.




Eu não entendo porque as respostas são tão complexas aqui.

Aqui está um encerramento:

var a = 42;

function b() { return a; }

Sim. Você provavelmente usa isso muitas vezes ao dia.


Não há razão para acreditar que os fechamentos sejam um hack de design complexo para resolver problemas específicos. Não, encerramentos são apenas sobre o uso de uma variável que vem de um escopo mais alto da perspectiva de onde a função foi declarada (não executada) .

Agora o que isso permite que você faça pode ser mais espetacular, veja outras respostas.




Ok, conversando com uma criança de 6 anos, eu possivelmente usaria as seguintes associações.

Imagine - você está brincando com seus irmãos e irmãs pequenos em toda a casa, e você está se movendo com seus brinquedos e trouxe alguns deles para o quarto do irmão mais velho. Depois de um tempo seu irmão voltou da escola e foi para seu quarto, e ele trancou dentro dele, então agora você não podia mais acessar brinquedos deixados lá de uma maneira direta. Mas você poderia bater a porta e pedir a seu irmão por esses brinquedos. Isso é chamado de fechamento do brinquedo ; seu irmão inventou para você e agora ele está no escopo externo .

Compare com uma situação em que uma porta foi trancada por rascunho e ninguém dentro (execução de função geral), e então algum incêndio local e queima a sala (coletor de lixo: D), e então uma nova sala foi construída e agora você pode sair outros brinquedos lá (nova instância de função), mas nunca obter os mesmos brinquedos que foram deixados na primeira instância da sala.

Para uma criança avançada eu colocaria algo como o seguinte. Não é perfeito, mas faz você se sentir sobre o que é:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Como você pode ver, os brinquedos deixados na sala ainda são acessíveis através do irmão e não importa se a sala está trancada. Aqui está um jsbin para brincar com ele.




Fechamentos são difíceis de explicar porque eles são usados ​​para fazer algum trabalho de comportamento que todo mundo intuitivamente espera que funcione de qualquer maneira. Acho que a melhor maneira de explicá-los (e o modo como aprendi o que eles fazem) é imaginar a situação sem eles:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

O que aconteceria aqui se o JavaScript não conhecesse closures? Basta substituir a chamada na última linha pelo seu corpo de método (que é basicamente o que as chamadas de função fazem) e você obtém:

console.log(x + 3);

Agora, onde está a definição de x ? Nós não definimos no escopo atual. A única solução é deixar o plus5 carregar seu escopo (ou melhor, o escopo de seus pais) por aí. Desta forma, x é bem definido e está ligado ao valor 5.




Como eu explicaria isso para uma criança de seis anos:

Você sabe como os adultos podem ter uma casa e eles a chamam de lar? Quando uma mãe tem um filho, a criança realmente não possui nada, certo? Mas seus pais possuem uma casa, então sempre que alguém perguntar à criança "Onde está sua casa?", Ela pode responder "aquela casa!" E apontar para a casa de seus pais. Um "Encerramento" é a capacidade da criança de sempre (mesmo se no exterior) poder dizer que tem uma casa, mesmo que sejam os pais que possuem a casa.




Uma função em JavaScript não é apenas uma referência a um conjunto de instruções (como na linguagem C), mas também inclui uma estrutura de dados oculta que é composta de referências a todas as variáveis ​​não-locais que ele usa (variáveis ​​capturadas). Essas funções de duas peças são chamadas de fechamentos. Cada função em JavaScript pode ser considerada um encerramento.

Fechamentos são funções com um estado. É um pouco semelhante a "this" no sentido de que "this" também fornece estado para uma função, mas function e "this" são objetos separados ("this" é apenas um parâmetro sofisticado e a única maneira de vinculá-lo permanentemente a um função é criar um fechamento). Enquanto "this" e função sempre vivem separadamente, uma função não pode ser separada de seu fechamento e a linguagem não fornece meios para acessar variáveis ​​capturadas.

Como todas essas variáveis ​​externas referenciadas por uma função lexicalmente aninhada são, na verdade, variáveis ​​locais na cadeia de suas funções lexicamente delimitadoras (variáveis ​​globais podem ser consideradas variáveis ​​locais de alguma função raiz), e cada execução de uma função cria novas instâncias de Em suas variáveis ​​locais, toda execução de uma função retornando (ou transferindo-a de outra maneira, como registrá-la como um retorno de chamada) uma função aninhada cria um novo fechamento (com seu próprio conjunto de variáveis ​​não-locais referenciadas que representam sua execução) contexto).

Além disso, deve ser entendido que variáveis ​​locais em JavaScript são criadas não no quadro da pilha, mas no heap e destruídas somente quando ninguém as está referenciando. Quando uma função retorna, referências a suas variáveis ​​locais são decrementadas, mas elas ainda podem ser não nulas se durante a execução atual elas se tornarem parte de um encerramento e ainda forem referenciadas por suas funções aninhadas lexicalmente (o que pode acontecer somente se as referências a essas funções aninhadas foram retornadas ou transferidas para algum código externo).

Um exemplo:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();



Eu simplesmente os apontaria para a página do Mozilla Closures . É a melhor, mais concisa e simples explicação sobre o básico do fechamento e o uso prático que encontrei. É altamente recomendado para qualquer pessoa que aprenda JavaScript.

E sim, eu até o recomendaria para uma criança de 6 anos - se a criança de 6 anos estiver aprendendo sobre fechamentos, é lógico que eles estão prontos para compreender a explicação concisa e simples fornecida no artigo.




Levando a questão a sério, devemos descobrir o que uma criança típica de 6 anos é capaz de cognitivamente, embora, reconhecidamente, alguém que esteja interessado em JavaScript não seja tão típico.

Em Childhood Development: 5 to 7 Years diz:

Seu filho será capaz de seguir as instruções em duas etapas. Por exemplo, se disser ao seu filho: "Vá até a cozinha e me traga um saco de lixo", ele será capaz de se lembrar dessa direção.

Podemos usar este exemplo para explicar os encerramentos, da seguinte maneira:

A cozinha é um fechamento que possui uma variável local, chamada trashBags . Há uma função dentro da cozinha chamada getTrashBag que pega um saco de lixo e o devolve.

Podemos codificar isso em JavaScript assim:

function makeKitchen () {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

kitchen.getTrashBag(); // returns trash bag C
kitchen.getTrashBag(); // returns trash bag B
kitchen.getTrashBag(); // returns trash bag A

Mais pontos que explicam porque os fechamentos são interessantes:

  • Cada vez que makeKitchen() é chamado, um novo fechamento é criado com seus próprios trashBags separados.
  • A variável trashBags é local para o interior de cada cozinha e não é acessível por fora, mas a função interna da propriedade getTrashBag tem acesso a ela.
  • Cada chamada de função cria um fechamento, mas não haveria necessidade de manter o fechamento a menos que uma função interna, que tenha acesso ao interior do fechamento, possa ser chamada de fora do fechamento. Retornar o objeto com a função getTrashBag faz isso aqui.



Um fechamento é quando uma função interna tem acesso a variáveis ​​em sua função externa. Essa é provavelmente a explicação mais simples de uma linha que você pode obter para encerramentos.




OK, 6 anos de idade fecha fã. Você quer ouvir o exemplo mais simples de fechamento?

Vamos imaginar a próxima situação: um motorista está sentado em um carro. Esse carro está dentro de um avião. Avião está no aeroporto. A capacidade do motorista para acessar as coisas fora de seu carro, mas dentro do avião, mesmo se o avião sair de um aeroporto, é um fechamento. É isso aí.Quando você fizer 27 anos, veja a explicação mais detalhada ou o exemplo abaixo.

Aqui está como eu posso converter minha história de avião no código.

var plane = function (defaultAirport) {

    var lastAirportLeft = defaultAirport;

    var car = {
        driver: {
            startAccessPlaneInfo: function () {
                setInterval(function () {
                    console.log("Last airport was " + lastAirportLeft);
                }, 2000);
            }
        }
    };
    car.driver.startAccessPlaneInfo();

    return {
        leaveTheAirport: function (airPortName) {
            lastAirportLeft = airPortName;
        }
    }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");




Você está tendo um sono e convida o Dan. Você diz a Dan para trazer um controlador XBox.

Dan convida Paul. Dan pede a Paul para trazer um controlador. Quantos controladores foram trazidos para a festa?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");



Eu tendo a aprender melhor por comparações BOM / BAD. Eu gosto de ver código de trabalho seguido por código não funcional que alguém provavelmente encontrará. Eu coloquei um jsFiddle que faz uma comparação e tenta reduzir as diferenças para as explicações mais simples que eu poderia criar.

Fechamentos feitos direito:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • No código acima createClosure(n)é invocado em cada iteração do loop. Observe que eu nomeei a variável npara destacar que é uma nova variável criada em um novo escopo de função e não é a mesma variável indexque está vinculada ao escopo externo.

  • Isso cria um novo escopo e nestá vinculado a esse escopo; Isso significa que temos 10 escopos separados, um para cada iteração.

  • createClosure(n) retorna uma função que retorna o n dentro desse escopo.

  • Dentro de cada escopo nestá vinculado a qualquer valor que tenha quando createClosure(n)foi invocado, portanto, a função aninhada que é retornada sempre retornará o valor de nquando ela createClosure(n)foi invocada.

Fechamentos feitos erradamente:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • No código acima, o loop foi movido dentro da createClosureArray()função e a função agora retorna a matriz concluída, o que à primeira vista parece mais intuitivo.

  • O que pode não ser óbvio é que since createClosureArray()é chamado apenas uma vez que apenas um escopo é criado para essa função, em vez de um para cada iteração do loop.

  • Dentro dessa função, uma variável nomeada indexé definida. O loop é executado e adiciona funções à matriz que retorna index. Note que indexé definido dentro da createClosureArrayfunção que só é invocada uma vez.

  • Porque havia apenas um escopo dentro da createClosureArray()função, indexsó é vinculado a um valor dentro desse escopo. Em outras palavras, cada vez que o loop altera o valor de index, ele o altera para tudo que faz referência a ele dentro desse escopo.

  • Todas as funções adicionadas à matriz retornam a indexvariável SAME do escopo pai onde ela foi definida, em vez de 10 diferentes de 10 escopos diferentes, como o primeiro exemplo. O resultado final é que todas as 10 funções retornam a mesma variável do mesmo escopo.

  • Após o loop terminar e indexterminar de ser modificado, o valor final era 10, portanto, cada função adicionada à matriz retorna o valor da única indexvariável que agora está definida como 10.

Resultado

CLASSES FEITAS À DIREITA
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

CLASSES FEITAS ERRADAS
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10




Eu escrevi um post no blog um tempo atrás explicando os fechamentos. Aqui está o que eu disse sobre encerramentos em termos de por que você iria querer um.

Fechamentos são uma maneira de permitir que uma função tenha variáveis ​​privadas persistentes - isto é, variáveis ​​que apenas uma função conhece, onde ela pode acompanhar as informações de tempos anteriores que foram executadas.

Nesse sentido, eles permitem que uma função atue um pouco como um objeto com atributos privados.

Postagem completa:

Então, quais são essas coisas de fechamento?




Eu montei um tutorial interativo em JavaScript para explicar como o fechamento funciona. O que é um encerramento?

Aqui está um dos exemplos:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here



O autor de Closures explicou muito bem os encerramentos, explicando o motivo pelo qual precisamos deles e também explicando o ambiente lexical necessário para entender os encerramentos.
Aqui está o resumo:

E se uma variável for acessada, mas não for local? Como aqui:

Nesse caso, o intérprete encontra a variável no LexicalEnvironmentobjeto externo .

O processo consiste em duas etapas:

  1. Primeiro, quando uma função f é criada, ela não é criada em um espaço vazio. Existe um objeto LexicalEnvironment atual. No caso acima, é a janela (a é indefinida no momento da criação da função).

Quando uma função é criada, ela obtém uma propriedade oculta, chamada [[Scope]], que faz referência ao LexicalEnvironment atual.

Se uma variável é lida, mas não pode ser encontrada em nenhum lugar, um erro é gerado.

Funções aninhadas

As funções podem ser aninhadas uma dentro da outra, formando uma cadeia de LexicalEnvironments que também pode ser chamada de cadeia de escopo.

Então, a função g tem acesso a g, a e f.

Fechamentos

Uma função aninhada pode continuar a viver após a conclusão da função externa:

Marcando o LexicalEnvironments:

Como vemos, this.sayé uma propriedade no objeto de usuário, portanto, continua a viver após o usuário ter concluído.

E se você se lembra, quando this.sayé criado, ele (como toda função) obtém uma referência interna this.say.[[Scope]]ao LexicalEnvironment atual. Portanto, o LexicalEnvironment da atual execução do usuário permanece na memória. Todas as variáveis ​​do usuário também são suas propriedades, então elas também são cuidadosamente mantidas, e não descartadas como normalmente.

O ponto principal é garantir que, se a função interna quiser acessar uma variável externa no futuro, ela seja capaz de fazê-lo.

Para resumir:

  1. A função interna mantém uma referência ao LexicalEnvironment externo.
  2. A função interna pode acessar variáveis ​​a qualquer momento, mesmo que a função externa esteja terminada.
  3. O navegador mantém o LexicalEnvironment e todas as suas propriedades (variáveis) na memória até que haja uma função interna que faça referência a ele.

Isso é chamado de fechamento.






Related