Atualizado: 07/12/2024

Iteradores - JavaScript

Iteradores são uma abstração para percorrer conjuntos de dados, sendo utilizados para organizar o acesso sequencial a elementos de conjuntos de dados como arrays, objetos Set, Map, strings, entre outros. Por meio dos iteradores, podemos percorrer um conjunto de dados, como um array, utilizando o ciclo for..of:

const people = ["Tom", "Bob", "Sam"];
for (const person of people) {
    console.log(person);
}

No ciclo for..of, à direita do operador of, especificamos o conjunto de dados ou objeto iterável (aquilo que chamamos de Iterable), do qual podemos extrair elementos individuais no ciclo. A capacidade de percorrer um objeto, como um array no exemplo acima, é viabilizada porque esses objetos utilizam iteradores. Vamos explorar mais sobre o que são iteradores e como criar um iterador personalizado.

Obtendo um Iterador

Qualquer objeto iterável, como um array, Map, Set etc., armazena em sua propriedade Symbol.iterator uma função que retorna um iterador associado ao objeto:

const people = ["Tom", "Bob", "Sam"];
// obtemos o iterador do array
const iterator = people[Symbol.iterator]();
console.log(iterator);  // Array Iterator {}

Aqui, obtemos o iterador do array, portanto, será exibido algo como Array Iterator {} na console.

Outro exemplo envolve uma string que também é um objeto iterável e pode ser percorrida caracter por caracter:

const username = "Tom";
for (let char of username) {
    console.log(char);
}

Para a string, também podemos obter um iterador:

const username = "Tom";
// obtemos o iterador da string
const iterator = username[Symbol.iterator]();
console.log(iterator);  // StringIterator {}

O iterador da string é do tipo StringIterator. De modo semelhante, podemos obter iteradores para outros tipos de objetos iteráveis.

É importante observar que diferentes tipos podem ter métodos adicionais para obter um iterador. Por exemplo, os arrays têm o método entries(), que também retorna um iterador do array:

const people = ["Tom", "Bob", "Sam"];
console.log(people.entries()); // Array Iterator {}

Método next dos Iteradores

Os iteradores fornecem o método next(), que retorna um objeto com duas propriedades: value e done.

{value, done}

A propriedade value armazena o valor do elemento atual sendo percorrido. A propriedade done indica se ainda existem objetos na coleção disponíveis para serem percorridos. Se ainda houver elementos, a propriedade done é false. Se não houver mais elementos disponíveis para percorrer, essa propriedade será true, e o método next() retornará um objeto:

{done: true}

Por exemplo:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
const result = iter.next();
console.log(result);  // {value: "Tom", done: false}

Aqui, ao chamarmos o método next(), obtemos o primeiro resultado do iterador:

{value: "Tom", done: false}

Podemos ver que o objeto atual representa a string "Tom", e o valor done: false indica que ainda existem elementos no array para percorrer.

Podemos chamar o método next() várias vezes para obter outros elementos do array:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
console.log(iter.next());  // {value: "Tom", done: false}
console.log(iter.next());  // {value: "Bob", done: false}
console.log(iter.next());  // {value: "Sam", done: false}
console.log(iter.next());  // {value: undefined, done: true}

Usando o método next(), podemos percorrer todos os objetos do array manualmente:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
let item;
while (!(item = iter.next()).done) {
    console.log(item.value);
}

No ciclo while, do método next() do iterador, obtemos o objeto atual na variável item: item = iter.next()

E verificamos sua propriedade done: se for false (ou seja, ainda existem elementos no conjunto), continuamos o ciclo.

No ciclo, acessamos a propriedade value do objeto obtido:

console.log(item.value);

Saída do console:

Tom
Bob
Sam

No entanto, isso é redundante, pois todas as coleções que retornam iteradores suportam a iteração usando o ciclo for..of, que justamente utiliza o iterador para obter os elementos.

Criando seu próprio Iterador

Para criar um iterador que percorre um array de trás para frente, podemos implementar uma função que define esse comportamento específico:

const people = ["Tom", "Bob", "Sam"];

function reverseArrayIterator(array) {
    let count = array.length;
    return {
        next: function() {
            if (count > 0) {
                return {
                    value: array[--count],
                    done: false
                };
            } else {
                return {
                    value: undefined,
                    done: true
                };
            }
        }
    };
};

const iter = reverseArrayIterator(people);
let item;
while (!(item = iter.next()).done) {
    console.log(item.value);
}

Aqui, a variável count é inicializada com o valor do tamanho do array. A função retorna um objeto iterador cujo método next() implementa a lógica de iteração: se o contador count for maior que 0 (ou seja, ainda há elementos para percorrer), next() retorna um objeto cuja propriedade done é false (indicando que o iterador ainda não alcançou o início do array), e a propriedade value contém o elemento correspondente do array, apontado pela variável count após o decremento.

Quando a variável count chega a 0 (ou seja, o iterador atingiu o início), next() retorna um objeto cuja propriedade done é true e a propriedade value é undefined.

Dessa forma, obtemos um iterador que percorre os elementos do array de trás para frente. A saída no console seria:

Sam
Bob
Tom

Para fazer com que esse iterador seja usado também em ciclos for..of, podemos modificar o iterador padrão do array para usar nossa função de iteração reversa:

const people = ["Tom", "Bob", "Sam"];

function reverseArrayIterator() {
    const array = this;  // capturamos o contexto do array via 'this'
    let count = array.length;
    return {
        next: function() {
            if (count > 0) {
                return {
                    value: array[--count],
                    done: false
                };
            } else {
                return {
                    value: undefined,
                    done: true
                };
            }
        }
    };
};

// modificamos o iterador padrão do array people
people[Symbol.iterator] = reverseArrayIterator;

for (const person of people) {
    console.log(person);
}

Nesse exemplo, foram feitas duas mudanças principais:

  • Dentro da função iteradora, usamos this para referenciar o array atual.

  • A função do iterador é atribuída à propriedade Symbol.iterator do array people, substituindo o iterador padrão por nosso iterador personalizado.

Dessa forma, quando usamos um ciclo for..of no array people, ele agora usa o iterador personalizado, percorrendo os elementos de trás para frente.

Criando Objetos Iteráveis

Diferentes objetos podem ter suas próprias implementações de iteradores. Se necessário, podemos definir um objeto com seu próprio iterador. O uso de iteradores nos fornece uma maneira de criar um objeto que se comporta como uma coleção de elementos.

Para criar um objeto iterável, precisamos definir o método [Symbol.iterator]() no objeto. Este método, de fato, representará o iterador:

const iterable = {
    [Symbol.iterator]() {
        return {
        next() {
            // se ainda houver elementos
            return { value: ..., done: false };
            // se não houver mais elementos
            return { value: undefined, done: true };
        }
        };
    }
};

O [Symbol.iterator]() retorna um objeto que possui o método next(). Este método retorna um objeto com duas propriedades: value e done.

Se o nosso objeto tiver elementos, a propriedade value conterá o valor do elemento atual, e a propriedade done será false.

Se não houver mais elementos disponíveis, a propriedade done será true.

Por exemplo, vamos implementar um objeto iterável simples que retorna uma sequência de números:

const iterable = {
    [Symbol.iterator]() {
            return {
            current: 1,
            end: 3,
            next() {
                if (this.current <= this.end) {
                return { value: this.current++, done: false };
                }
                return { done: true };
            }
        };
    }
};
for (const value of iterable) {
    console.log(value);
}

Aqui, o iterador retorna números de 1 a 3. Para rastrear o elemento atual no objeto que retorna o método, são definidas duas propriedades:

current: 1,
end: 3,

O incremento this.current++ garante que na próxima chamada do método next, o valor de current seja um maior.

Se o limite for alcançado, então retornamos o objeto

return { done: true };

Isso indica que não há mais elementos disponíveis para percorrer.

Podemos obter os elementos retornados pelo iterador:

const myIterator = iterable[Symbol.iterator](); // obtemos o iterador
console.log(myIterator.next()); // {value: 1, done: false}
console.log(myIterator.next()); // {value: 2, done: false}
console.log(myIterator.next()); // {value: 3, done: false}
console.log(myIterator.next()); // {done: true}

Inicialmente, obtemos o iterador na constante myIterator. Então, ao acessar seu método next(), obtemos sequencialmente todos os elementos. Na quarta chamada do método next(), o iterador conclui o loop, retornando o objeto {done: true}.

No entanto, se quisermos percorrer nosso objeto e obter seus elementos, não precisamos acessar o método next(). Como o objeto iterable implementa um iterador, ele pode ser percorrido usando o loop for..of:

for (const value of iterable) {
    console.log(value);
}

Saída do console:

1
2
3

O loop for..of automaticamente acessa o método next() e extrai o valor.

Vejamos outro exemplo:

// objeto da empresa
const company = {
    // array de funcionários
    employees: [
        {name: "Tom", age: 39, position: "Senior Developer"},
        {name: "Bob", age: 43, position: "Middle Developer"},
        {name: "Sam", age: 28, position: "Junior Developer"},
    ]
};
// estabelecemos um iterador
company[Symbol.iterator] = function() {
    const array = this.employees; // obtemos o array de funcionários
    let current = 0;
    return {
        next() {
        if (current < array.length) {
            return { value: array[current++].name, done: false };
        }
        return { value: undefined, done: true };
        }
    };
};

for (const employee of company) {
    console.log(employee);
}

Aqui, o objeto company representa uma empresa fictícia com um array de funcionários employees. Suponhamos que, com o iterador, queremos obter o nome de cada funcionário. Para isso, definimos uma função iteradora no objeto company, que percorre todos os elementos do array employees. Saída do console da aplicação:

Tom
Bob
Sam
Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com