Usando declarações let de variáveis ​​e recursos dos fechamentos resultantes em JavaScript

Fui inspirado a escrever esta nota lendo o artigo sobre o Habré "Var, let ou const? Problemas dos escopos das variáveis ​​e ES6 ” e comentários aos mesmos, bem como a parte correspondente do livro de Zakas N.“ Entendendo o ECMAScript 6 ” . Com base no que li, cheguei à conclusão de que nem tudo é tão simples na avaliação do uso de var ou let . Autores e comentaristas tendem a acreditar que, na ausência de necessidade de oferecer suporte a versões mais antigas de navegadores, faz sentido abandonar completamente o uso de var , além de usar algumas construções simplificadas, em vez das antigas, por padrão.

Já foi dito o suficiente sobre o escopo desses anúncios, inclusive nos materiais acima, então gostaria de focar apenas em alguns pontos não óbvios.

Para começar, gostaria de considerar expressões de funções chamadas imediatamente (Expressão de Função Invocada Imediatamente, IIFE) em loops.
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 1 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

ou você pode ficar sem eles usando let :

 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); }); /*     0 newECMA6add.js:4:37 1 newECMA6add.js:4:37 2 newECMA6add.js:4:37 */ 

Zakas N. afirma que os dois exemplos semelhantes, dando o mesmo resultado, também funcionam exatamente da mesma maneira:
"Esse loop funciona exatamente como o loop que usou var e um IIFE, mas é sem dúvida mais limpo"
que, no entanto, ele próprio, um pouco mais, refuta indiretamente.

O fato é que cada iteração do loop ao usar let cria uma variável local separada i , enquanto a ligação nas funções enviadas à matriz também vai separar variáveis ​​de cada iteração.

Nesse caso em particular, o resultado realmente não é diferente, mas e se complicarmos um pouco o código?
 let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

Aqui, adicionando ++ i, nosso resultado se mostrou bastante previsível, já que chamamos a função com valores i que eram relevantes no momento da chamada, mesmo quando o próprio loop passou, portanto, a operação subsequente ++ i não afetou o valor passado para a função na matriz, uma vez que já foi fechado na função (i) com um valor específico de i .

Agora compare com a versão letiva sem IIFE
 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); /*    1 newECMA6add.js:4:37 3 newECMA6add.js:4:37 */ 

O resultado, aparentemente, mudou, e a natureza dessa mudança é que não chamamos a função com o valor imediatamente, mas a função assumiu os valores disponíveis nos fechamentos em iterações específicas do ciclo.

Para entender melhor a essência do que está acontecendo, considere exemplos com duas matrizes. E para iniciantes, vamos dar var, sem IIFE :
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    5 newECMA6add.js:6:37 6 newECMA6add.js:6:37 7 newECMA6add.js:5:37 8 newECMA6add.js:5:37 */ 

Tudo é óbvio até agora - não há fechamento (embora possamos dizer que sim, mas no escopo global, embora isso não esteja totalmente correto, pois o acesso a i está essencialmente em todo lugar), ou seja, da mesma forma, mas com uma área local aparentemente, a variável i terá uma entrada semelhante:
 let func1 = [], func2 = []; function test() { for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } } test(); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*     5 newECMA6add.js:7:41 6 newECMA6add.js:7:41 7 newECMA6add.js:6:41 8 newECMA6add.js:6:41 */ 

Nos dois exemplos, ocorre o seguinte:

1. No início da última iteração do ciclo i == 2 , então incrementado por 1 (++ i) , e no final 1 é adicionado mais a partir de i ++ . Como resultado, no final de todo o ciclo i == 4 .

2. As funções localizadas nas matrizes func1 e func2 são chamadas uma a uma , e em cada uma delas a mesma variável i é incrementada sequencialmente, que é encerrada em relação ao seu escopo, o que é especialmente perceptível quando não estamos lidando com uma variável global, mas com uma local.

Adicione IIFE .
A primeira opção:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 */ 
A segunda opção:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(i); } }(++i)); func1.push(function(i) { return function() { console.log(i); } }(++i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:56 1 newECMA6add.js:5:56 */ 

Ao adicionar IIFE no primeiro caso, simplesmente chamamos os valores fixos de i na função (i) ( 0 e 2 , durante a primeira e a segunda passagem do ciclo, respectivamente), e os incrementamos em 1, cada função é separada da outra, pois aqui está o fechamento de uma variável comum não há loop, devido ao fato de que o valor i foi transmitido imediatamente durante a passagem do loop. No segundo caso, também não há fechamento para a variável de loop, mas o valor foi transmitido com incremento simultâneo, portanto, no final da primeira passagem i == 4 , e o loop não foi mais longe. Mas, chamo a atenção para o fato de que o fechamento de variáveis ​​de funções externas em funções internas, para cada função separadamente, ainda está presente na primeira e na segunda variantes. Por exemplo:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 2 newECMA6add.js:6:56 4 newECMA6add.js:6:56 2 newECMA6add.js:5:56 4 newECMA6add.js:5:56 */ 
nota: mesmo se você enquadrar o ciclo com uma função, fechamentos comuns naturalmente não serão.

Agora considere a instrução let , sem IIFE, respectivamente.
 let func1 = [], func2 = []; for (let i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:41 4 newECMA6add.js:6:41 3 newECMA6add.js:5:41 5 newECMA6add.js:5:41 */ 

E aqui, novamente formamos um curto-circuito para a variável do loop, e não um, mas dois, e não separados, mas comuns, o que é lógico, dado o conhecido princípio de ciclos let in.

Como resultado, temos que no primeiro fechamento, antes de chamar as funções nas matrizes, o valor é i == 1 e no segundo i == 3 . Esses são os valores que a variável que eu recebi antes do i ++ e da iteração do loop, mas depois de todas as instruções no bloco do loop e eles estão fechados para cada iteração específica.

Em seguida, as funções localizadas na matriz func1 são chamadas e incrementam as variáveis ​​correspondentes nos dois fechamentos e, como resultado, no primeiro i == 2 e no segundo i == 4 .

A chamada subsequente para func2 aumenta ainda mais e obtém i == 3 e 5, respectivamente.

Eu deliberadamente coloquei func2 e func1 dentro do bloco de forma que a independência de sua localização fosse mais claramente visível e para enfatizar a atenção do leitor ao fato de fechar as variáveis ​​em loop.

Concluindo, darei um exemplo trivial que visa reforçar o entendimento de fechamentos e o escopo de let :
 let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i); /* 1 newECMA6add.js:5:34 ReferenceError: i is not definednewECMA6add.js:10:1 */ 

O que temos no total


1. Invocar expressões de funções chamadas imediatamente não é equivalente a usar variáveis let iteráveis ​​em funções em loops e, em alguns casos, leva a resultados diferentes.

2. Devido ao fato de que ao usar uma declaração let para um iterador, uma variável local separada é criada em cada iteração, surge a pergunta sobre o descarte de dados desnecessários pelo coletor de lixo. Neste ponto, admito, eu queria inicialmente concentrar a atenção, suspeitando que a criação de um grande número de variáveis ​​em grandes, respectivamente, loops retardaria o compilador; no entanto, ao classificar uma matriz de teste usando apenas declarações de variáveis permitidas , ele mostrou um ganho no tempo de execução de quase duas vezes para uma matriz de 100.000 células:
Opção com var:
 const start = Date.now(); var arr = [], func1 = [], func2 = []; for (var i = 0; i < 100000; i++) { arr.push(Math.random()); } for (var i = 0; i < 99999; i++) { var min, minind = i; for (var j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function(i) { return function() { return i; } }(arr[i])); } func1.push(function(i) { return function() { return i; } }(arr[99999])); for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 9.847 


E a opção com let:
 const start = Date.now(); let arr = [], func1 = [], func2 = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } for (let i = 0; i < 99999; i++) { let min, minind = i; for (let j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function() { return arr[i]; }); } func1.push(function() { return arr[99999]; }); for (let i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 5.3 


Ao mesmo tempo, o tempo de execução era praticamente independente da presença / ausência de instruções:

com IIFE
 func1.push(function(i) { return function() { return i; } }(arr[i])); 

ou
sem IIFE
 func1.push(function() { return arr[i]; }); 

e
chamada de função
 for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } 


Nota: Entendo que as informações sobre velocidade não são novas, mas, para completar, acho que vale a pena dar esses dois exemplos.

De tudo isso, podemos concluir que o uso de declarações let em vez de var , em aplicativos que não exigem compatibilidade com os padrões anteriores, é mais do que justificado, especialmente em casos com loops. Mas, ao mesmo tempo, vale lembrar as características do comportamento em situações de fechamento e, se necessário, continuar usando expressões de funções chamadas imediatamente.

Source: https://habr.com/ru/post/pt462971/


All Articles