Objetos iteráveis são uma generalização de arrays. Esse é um conceito que nos permite tornar qualquer objeto utilizável em um loop for..of
.
Claro, Arrays são iteráveis. Mas existem muitos outros objetos integrados que também são iteráveis. Por exemplo, strings também são iteráveis.
Se um objeto não é tecnicamente um array, mas representa uma coleção (lista, conjunto) de algo, então for..of
é uma ótima sintaxe para fazer um loop sobre ele, então vamos ver como fazê-lo funcionar.
Podemos facilmente compreender o conceito de iteráveis criando um nosso próprio.
Por exemplo, temos um objeto que não é um array, mas parece adequado para for..of
.
Como um objeto range
que representa um intervalo de números:
deixe intervalo = { de: 1, para: 5 }; // Queremos que for..of funcione: // for(deixe num do intervalo) ... num=1,2,3,4,5
Para tornar o objeto range
iterável (e assim permitir que for..of
funcione), precisamos adicionar um método ao objeto chamado Symbol.iterator
(um símbolo integrado especial apenas para isso).
Quando for..of
inicia, ele chama esse método uma vez (ou erros se não for encontrado). O método deve retornar um iterador – um objeto com o método next
.
Em diante, for..of
funciona apenas com aquele object retornado .
Quando for..of
deseja o próximo valor, ele chama next()
nesse objeto.
O resultado de next()
deve ter o formato {done: Boolean, value: any}
, onde done=true
significa que o loop foi concluído, caso contrário, value
é o próximo valor.
Aqui está a implementação completa do range
com comentários:
deixe intervalo = { de: 1, para: 5 }; // 1. chama for..of inicialmente chama isso intervalo[Symbol.iterator] = function() { // ...retorna o objeto iterador: // 2. Adiante, for..of funciona apenas com o objeto iterador abaixo, solicitando os próximos valores retornar { atual: this.from, último: this.to, // 3. next() é chamado em cada iteração pelo loop for..of próximo() { // 4. deve retornar o valor como um objeto {done:.., value :...} if (este.atual <= este.último) { return {feito: falso, valor: this.current++ }; } outro { retornar {feito: verdadeiro}; } } }; }; // agora funciona! for (seja num do intervalo) { alerta(núm); // 1, depois 2, 3, 4, 5 }
Observe o recurso principal dos iteráveis: separação de interesses.
O range
em si não possui o método next()
.
Em vez disso, outro objeto, o chamado “iterador” é criado pela chamada para range[Symbol.iterator]()
, e seu next()
gera valores para a iteração.
Portanto, o objeto iterador é separado do objeto sobre o qual ele itera.
Tecnicamente, podemos mesclá-los e usar o próprio range
como iterador para tornar o código mais simples.
Assim:
deixe intervalo = { de: 1, para: 5, [Símbolo.iterador]() { isto.atual = isto.de; devolva isso; }, próximo() { if (este.atual <= isto.to) { return {feito: falso, valor: this.current++ }; } outro { retornar {feito: verdadeiro}; } } }; for (seja num do intervalo) { alerta(núm); // 1, depois 2, 3, 4, 5 }
Agora range[Symbol.iterator]()
retorna o próprio objeto range
: ele possui o método next()
necessário e lembra o progresso da iteração atual em this.current
. Mais curto? Sim. E às vezes tudo bem também.
A desvantagem é que agora é impossível ter dois loops for..of
rodando sobre o objeto simultaneamente: eles compartilharão o estado da iteração, porque há apenas um iterador – o próprio objeto. Mas dois for-ofs paralelos são raros, mesmo em cenários assíncronos.
Iteradores infinitos
Iteradores infinitos também são possíveis. Por exemplo, o range
se torna infinito para range.to = Infinity
. Ou podemos criar um objeto iterável que gere uma sequência infinita de números pseudoaleatórios. Também pode ser útil.
Não há limitações no next
, ele pode retornar cada vez mais valores, isso é normal.
É claro que o loop for..of
sobre esse iterável seria infinito. Mas sempre podemos pará-lo usando break
.
Matrizes e strings são os iteráveis integrados mais amplamente usados.
Para uma string, for..of
faz um loop sobre seus caracteres:
for (deixe o caractere de "teste") { // dispara 4 vezes: uma vez para cada personagem alerta(char); // t, então e, então s, então t }
E funciona corretamente com pares substitutos!
deixe str = '??'; for (deixe char de str) { alerta(char); // ?, e então ? }
Para uma compreensão mais profunda, vamos ver como usar um iterador explicitamente.
Iremos iterar sobre uma string exatamente da mesma maneira que for..of
, mas com chamadas diretas. Este código cria um iterador de string e obtém valores dele “manualmente”:
deixe str = "Olá"; // faz o mesmo que // para (deixe char de str) alert(char); deixe iterador = str[Symbol.iterator](); enquanto (verdadeiro) { deixe resultado = iterator.next(); if (resultado.feito) quebrar; alerta(resultado.valor); // gera caracteres um por um }
Isso raramente é necessário, mas nos dá mais controle sobre o processo do que for..of
. Por exemplo, podemos dividir o processo de iteração: iterar um pouco, depois parar, fazer outra coisa e continuar mais tarde.
Dois termos oficiais parecem semelhantes, mas são muito diferentes. Certifique-se de entendê-los bem para evitar confusão.
Iteráveis são objetos que implementam o método Symbol.iterator
, conforme descrito acima.
Array-likes são objetos que possuem índices e length
, então se parecem com arrays.
Quando usamos JavaScript para tarefas práticas em um navegador ou qualquer outro ambiente, podemos encontrar objetos que são iteráveis ou semelhantes a arrays, ou ambos.
Por exemplo, strings são iteráveis ( for..of
funciona nelas) e semelhantes a array (elas têm índices numéricos e length
).
Mas um iterável pode não ser semelhante a um array. E vice-versa, um array semelhante a pode não ser iterável.
Por exemplo, o range
no exemplo acima é iterável, mas não semelhante a um array, porque não possui propriedades indexadas e length
.
E aqui está o objeto que é semelhante a um array, mas não é iterável:
let arrayLike = { // tem índices e comprimento => semelhante a um array 0: "Olá", 1: "Mundo", comprimento: 2 }; // Erro (sem Symbol.iterator) for (deixe item de arrayLike) {}
Tanto iteráveis quanto arrays geralmente não são arrays , eles não têm push
, pop
etc. Isso é bastante inconveniente se tivermos tal objeto e quisermos trabalhar com ele como se fosse um array. Por exemplo, gostaríamos de trabalhar com range
usando métodos de array. Como conseguir isso?
Existe um método universal Array.from que pega um valor iterável ou semelhante a um array e cria um Array
“real” a partir dele. Então podemos chamar métodos de array nele.
Por exemplo:
deixe arrayLike = { 0: "Olá", 1: "Mundo", comprimento: 2 }; deixe arr = Array.from(arrayLike); // (*) alerta(arr.pop()); // Mundo (método funciona)
Array.from
na linha (*)
pega o objeto, examina-o como iterável ou semelhante a um array, então cria um novo array e copia todos os itens para ele.
O mesmo acontece para um iterável:
// assumindo que o intervalo é retirado do exemplo acima deixe arr = Array.from (intervalo); alerta(arr); // 1,2,3,4,5 (a conversão de array paraString funciona)
A sintaxe completa de Array.from
também nos permite fornecer uma função de “mapeamento” opcional:
Array.from(obj[, mapFn, thisArg])
O segundo argumento opcional mapFn
pode ser uma função que será aplicada a cada elemento antes de adicioná-lo ao array, e thisArg
nos permite definir this
para ele.
Por exemplo:
// assumindo que o intervalo é retirado do exemplo acima //elevamos ao quadrado cada número deixe arr = Array.from(intervalo, num => num * num); alerta(arr); //1,4,9,16,25
Aqui usamos Array.from
para transformar uma string em um array de caracteres:
deixe str = '??'; // divide str em um array de caracteres deixe chars = Array.from(str); alerta(caracteres[0]); // ? alerta(caracteres[1]); // ? alerta(chars.length); //2
Ao contrário de str.split
, ele depende da natureza iterável da string e, assim como for..of
, funciona corretamente com pares substitutos.
Tecnicamente aqui faz o mesmo que:
deixe str = '??'; deixe caracteres = []; // Array.from internamente faz o mesmo loop for (deixe char de str) { caracteres.push(char); } alerta(caracteres);
…Mas é mais curto.
Podemos até construir slice
com reconhecimento de substituto:
function fatia(str, início, fim) { return Array.from(str).slice(início, fim).join(''); } deixe str = '???'; alerta(fatia(str, 1, 3) ); // ?? // o método nativo não suporta pares substitutos alerta(str.slice(1, 3) ); // lixo (duas peças de pares substitutos diferentes)
Objetos que podem ser usados em for..of
são chamados iterable .
Tecnicamente, os iteráveis devem implementar o método denominado Symbol.iterator
.
O resultado de obj[Symbol.iterator]()
é chamado de iterator . Ele lida com processos de iteração adicionais.
Um iterador deve ter o método chamado next()
que retorna um objeto {done: Boolean, value: any}
, aqui done:true
denota o fim do processo de iteração, caso contrário o value
é o próximo valor.
O método Symbol.iterator
é chamado automaticamente por for..of
, mas também podemos fazer isso diretamente.
Iteráveis integrados, como strings ou arrays, também implementam Symbol.iterator
.
O iterador de string conhece pares substitutos.
Objetos que possuem propriedades e length
indexados são chamados de array-like . Esses objetos também podem ter outras propriedades e métodos, mas não possuem os métodos integrados dos arrays.
Se olharmos dentro da especificação – veremos que a maioria dos métodos integrados assumem que funcionam com iteráveis ou semelhantes a arrays em vez de arrays “reais”, porque isso é mais abstrato.
Array.from(obj[, mapFn, thisArg])
cria um Array
real a partir de um obj
iterável ou semelhante a um array, e podemos então usar métodos de array nele. Os argumentos opcionais mapFn
e thisArg
nos permitem aplicar uma função a cada item.