javascript - Como os loops "for in, for of, foreach" percorrem o array? - Stack Overflow em Português
Sempre usei o loop for no qual o mesmo diferencia um pouco dos loops mencionados, mas a questão é de como estes loops percorrem o array como, por exemplo, o loop for normalmente segue estes passos para percorrer o array:
for (let i = 0; i <= 10; i ++) {
Inicie a variável i com o valor 0.
O valor da variável i é menor ou igual a 10?
Se for true execute o bloco de código.
No final incremente 1 na variável i.
Se for false, pare o loop e segue o fluxo de execução normal do código.
Agora, já nos loops mencionados no título da pergunta como eles funcionariam?
editada 9/09 às 11:42
perguntada 5/09 às 14:20
Felipe, faltou alguma coisas nas respostas abaixo? Pelo que entendi, elas respondem o que foi perguntado - a menos que eu tenha entendido errado, claro :-) – hkotsubo 5/10 às 18:00
O laço for é um padrão de projeto para repetir comandos com uma inicialização, um passo que sempre será executado em cada iteração e uma condição que determinará quando deve parar de repetir.
Algumas linguagens criaram sintaxe para permitir novos padrões de projeto que fazem coisas mais comuns, assim algumas situações podem ser demonstradas de forma mais simples e evitando certos erros, e eventualmente até conseguindo fazer alguma otimização (em outras pode haver um custo extra).
O JavaScript não tem um comando foreach, só uma função com esse nome e é só isso uma função que tem uma laço dentro, de forma geral não faz sentido iusá-lo, mas as pessoas adoram, é só custo sem vantagem, pelo contrário têm desvantagens.
Existe na linguagem só o for ... in e o for ... of. Diferenças que já foram explicadas em Qual é a diferença entre o for...of e o for..in?.
Então esses laços já pegam um objeto e varrem todos seus membros. A linguagem inicializa a variável para começar o primeiro elemento, a condição determinará quando os elementos acabaram e o passo é o que faz você ir para o próximo elemento. Dependendo do tipo de objeto a linguagem pode fazer isso de uma forma diferente para se adequar. Ele não faz diferente do que você deveria fazer na mesma situação, se você não faz igual pode ser que esteja fazendo errado.
var objeto = { a: 'A', b: 'B', c: 'C' };
for (var i in objeto) console.log(objeto[i]);
Na verdade seria mais ou menos assim:
var objeto = { a: "A", b: "B", c: "C" };
var obj = Object.entries(objeto);
const it = obj.values();
while (!(entry = it.next()).done) console.log(entry.value[1]);
var objeto = [1, 2, 3];
for (var i of objeto) console.log(i);
Se transforma mais ou menos em:
const it = objeto.values();
while (!(entry = it.next()).done) console.log(entry.value);
Pode ser que fique mais simplificado (depende de implementação):
while (i < objeto.length) console.log(objeto[i++]);
A grosso modo é isso e reforço que a forma exata de funcionamento depende de implementação, desde que siga a especificação.
editada 8/09 às 12:11
O modo while (ì < objecto.length) só funciona array. – Maury Developer 5/09 às 15:29
O for..in itera sobre as propriedades enumeráveis de um objeto, desde que as chaves sejam strings (pois as chaves também pode ser Symbol's).
Repare que isso vale para qualquer objeto, não somente arrays. Ex:
function forIn(obj) {
console.log(`${i} = ${obj[i]}`);
// testando com array
x[Symbol('simbolo')] = 'symbol'; // chave não é string, não é mostrada no for..in
Object.defineProperty(x, 'no', { value: 10, enumerable: false }); // não-enumerável, não é mostrada no for..in
Object.defineProperty(x, 'yes', { value: 10, enumerable: true });  // enumerável, é mostrada no for..in
// definindo propriedades nos protótipos, todos os arrays a terão
Array.prototype.bla = 'bla';
Object.prototype.all = 'all';
console.log('iterando no array');
forIn(x); // mostra 0, 1, 2, "yes", "bla" e "all"
console.log(Object.entries(x)); // mostra 0, 1, 2 e "yes" (não mostra o "bla" nem o "all")
// testando com um objeto qualquer
[Symbol('c')]: 3 // símbolo, não é mostrada no for..in
Object.defineProperty(obj, 'b', { value: 2, enumerable: false }); // não-enumerável, não é mostrada no for..in
console.log('iterando no objeto');
forIn(obj); // mostra "a" e "all"
console.log(Object.entries(obj)); // mostra somente "a"
// testando com string
let s = 'a😀';
console.log('iterando na string');
forIn(s); // mostra o "all" (setado no prototype de Object) e 3 "caracteres"
console.log(Object.entries(s)); // não mostra o "all"
A outra resposta disse que fazer for (let i in x) seria equivalente a percorrer os elementos de Object.entries(x), mas existe uma diferença: Object.entries não traz as propriedades setadas no protótipo do objeto (como as propriedades "bla" e "all" do exemplo acima, que foram definidas respectivamente em Array.prototype e Object.prototype). Já o for..in traz essas propriedades, então se você quisesse ignorá-las, teria que fazer algo do tipo:
if (obj.hasOwnProperty(i)) console.log(`${i} = ${obj[i]}`);
Aí sim o "bla" e "all" seriam ignorados.
No caso da string, é mais curioso ainda. Apesar dela só ter 2 "caracteres" (a letra "a" e o emoji), são retornados 4 propriedades: o "all" (herdado de Object.prototype) e os 3 índices que correspondem aos seus "caracteres". Na verdade o emoji foi quebrado em 2 partes, conforme explicado aqui.
Outro detalhe importante, no caso de arrays, é que a documentação diz que não é garantido que os elementos sejam retornados na ordem dos seus índices numéricos (There is no guarantee that for...in will return the indexes in any particular order). Testando no Chrome, vi que ele itera na ordem, mas isso pode não ocorrer em todas as implementações.
Outro caso em que dá diferença para um for tradicional é quando fazemos algo assim:
// imprime só o 5
for(let i in x) console.log(`${i} = ${x[i]}`);
// imprime 5 vezes "undefined" antes do 5
for(let i = 0; i < x.length; i++) console.log(`${i} = ${x[i]}`);
De qualquer forma, um ponto importante é que o for..in funciona com qualquer objeto, iterando pelas propriedades enumeráveis do mesmo.
O for..of, ao contrário do for..in, não funciona com qualquer objeto, mas somente com aqueles que são iteráveis. Isso inclui strings, arrays, e objetos array-like (como por exemplo o NodeList retornado por querySelectorAll). A documentação também explica como criar seu próprio tipo iterável.
function forOf(obj) {
x[Symbol('simbolo')] = 'symbol';
Object.defineProperty(x, 'no', { value: 10, enumerable: false });
Object.defineProperty(x, 'yes', { value: 10, enumerable: true });
forOf(x); // mostra 0, 1, 2
// com uma string
forOf(s); // mostra o "a" e o emoji
[Symbol('c')]: 3
Object.defineProperty(obj, 'b', { value: 2, enumerable: false });
forOf(obj); // TypeError: obj is not iterable
Repare que agora as propriedades "extras", sejam ela enumeráveis ou não, definidas no próprio objeto ou no protótipo, são todas ignoradas.
No caso do array, somente os elementos são mostrados, pois o iterador de um array foi definido para funcionar assim: começa do índice zero e vai incrementando, até chegar ao length do array - por isso o caso abaixo dá diferença para um for..in:
x.bla = 'bla';
// imprime só o 3 e "bla"
console.log('for..in só pega as propriedades que existem');
for(let i in x) console.log(x[i]);
// imprime 3 vezes "undefined" antes do 3, depois outro "undefined"
console.log('for..of itera por todos os índices, até o length');
for(let i of x) console.log(i);
Repare no caso acima: for..in só iterou pelas propriedades existentes (no caso, o "3" e "bla"). Já for..of iterou pelos índices numéricos, começando do zero e indo até length - 1 (ou seja, tentou acessar os índices 0, 1 e 2, que como não existem, resultam em undefined, depois acessou o índice 3, e depois tentou acessar o índice 4 - já que o length é 5, então o loop vai até o índice 4 - que como não existe, também resulta em undefined).
No caso da string, o emoji não é mais "quebrado" e é exibido corretamente, pois o iterador de String garante este comportamento (ele itera sobre os code points da mesma - leia a pergunta já indicada acima para entender melhor).
E no caso do objeto, dá erro porque ele não é iterável (TypeError: obj is not iterable). Para que funcionasse com o mesmo, eu teria que definir um iterador para ele. Algo assim:
a: 10, b: 20
// iterador: retorna os valores das propriedades deste objeto
for (let p in this) yield this[p];
for (let i of obj) console.log(i); // imprime 10 e 20
No caso, eu usei for..in internamente para iterar pelas propriedades, e fiz o iterador retornar o respectivo valor da propriedade (mas eu poderia ter usado qualquer outra lógica interna para retornar os valores que eu quisesse, retornando inclusive valores que não necessariamente estão relacionados com as propriedades do objeto). Desta forma é possível iterar neste objeto com for..of.
No caso dos objetos nativos que podem ser iterados com for..of (como arrays e strings), eles internamente já implementam seu próprio iterador. Para os demais, basta seguir a documentação, que dá instruções para deixar seus objetos de acordo com os protocolos de iteração.
Ao contrário de for..in e for..of, que são construções da linguagem, forEach é um método do Array (embora não seja exclusivo deste, pois vários outros tipos também possuem um método similar, como Set, NodeList, etc).
Uma coisa que todos têm em comum é que você deve passar uma função de callback que é chamada para cada um dos elementos sendo percorridos. Por exemplo, no caso de arrays, ele itera pelos índices numéricos do mesmo, e no callback você passa o elemento e o respectivo índice:
let x = ['a', 'b', 'c'];
x.forEach(function (elemento, indice) { // imprime os índices e os respectivos elementos
console.log(`x[${indice}] = ${elemento}`);
A forma como ele funciona varia conforme o tipo. No caso de arrays, ele percorre os índices numéricos, até atingir o length do mesmo, mas ao contrário de for..of, o forEach verifica se o índice existe (veja o algoritmo na especificação). Daí que podemos ter situações como essa:
x.forEach(function (elemento, indice) { // imprime somente o 2 (os índices 0 e 1 não existem)
x.forEach(function (elemento, indice) { // imprime somente o 2 e o 5 (os outros índices não existem)
Já outros tipos podem implementar o forEach de maneira diferente. Por exemplo, um Set não define chaves, então os seus valores acabam sendo as próprias "chaves". Em um Map o forEach também itera pelas chaves não numéricas, etc. Cada tipo implementa o seu forEach de acordo com o que faz sentido para o seu propósito.