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(); });
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(); });
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(); });
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(); });
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(); });
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(); });
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(); });
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(); });
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(); });
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(); });
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);
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);
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);
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.