JavaScript

В JavaScript много операторов цикла:

  • оператор while
  • оператор do...while
  • оператор for
  • оператор for...in
  • оператор for...of

Их основная функция: повторять действия до тех пор, пока не будет выполнено определенное условие.

В этой статье мы узнаем, как работает оператор for...of и, где его следует использовать при написании кода в приложениях JS.

Skillbox

for…of

Оператор for...of относится к типу оператора for, который циклически повторяет итерируемые объекты ( iterable objects)), пока не достигнет конца строки.

Рассмотрим базовый пример:

let arr = [2,4,6,8,10]

for(let a of arr) {
    log(a)
}
// It logs:
// 2
// 4
// 6
// 8
// 10

Цикл for...of через массив arr выполнен с меньшим количеством кода, чем при использовании цикла for.

let myname = "Nnamdi Chidume"

for (let a of myname) {
    log(a)
}

// It logs:
// N
// n
// a
// m
// d
// i
//
// C
// h
// i
// d
// u
// m
// e

При использовании цикла for приходится задействовать математику и логику, чтобы рассчитать момент достижения конца myname и прекратить процесс. Однако с цикломfor...ofможно забыть о лишней головной боли :).

У цикла for...of есть одно общее определение:

for ( variable of iterable) {    //...}

variable хранит значение всех свойств итерируемого объекта на каждой итерации. iterable — итерируемый объект.

Итерируемые объекты и итераторы

В определении цикла for…of сказано, что он “циклически повторяет итерируемые объекты ( iterables)”. Таким образом, цикл for...of не может быть использован, если объект, вокруг которого должен быть совершен цикл, не является итерируемым.

Что такое итерируемые объекты?

Проще говоря, это объекты, на которых можно выполнить итерацию. В ECMAScript 2015 было внесено несколько дополнений. К примеру, новые протоколы, такие как протокол Iterator и протокол Iterable.

По словам разработчика Mozilla, “Благодаря итерируемому протоколу объекты JavaScript могут определять или настраивать поведение итерации, например, какие значения повторяются циклически в конструкции for..of.” и “чтобы быть итерируемым, объект реализует метод @@iterator, означающий, что объект (или один из объектов в цепочке прототипов) должен иметь свойство с ключом @@iterator, которое доступно через константу Symbol.iterator.”

Это означает, что объекты должны обладать свойством @@iterator, чтобы использоваться в цикле for...of, в соответствии с протоколом iterable.

Поэтому, когда объект со свойством @@iterator повторяется в цикле for...of, метод @@iterator вызывается тем же for...of. Метод @@iterator должен возвращать итератор.

Протокол Iterator определяет способ, с помощью которого поток значений возвращается из объекта. Итератор реализует метод next. Метод next обладает следующим рядом правил:

  • Он должен возвращать объект со свойствами done, value {done, value}
  • done относится к типу Boolean и указывает на достижение конца потока.
  • value содержит значение текущего цикла.

Пример:

const createIterator = function () {
    var array = ['Nnamdi','Chidume']
    return  {
        next: function() {
            if(this.index == 0) {
                this.index++
                return { value: array[this.index], done: false }
            }
            if(this.index == 1) {
                return { value: array[this.index], done: true }
            }
        },
        index: 0 
    }
}
const iterator = createIterator()
log(iterator.next()) // Nnamdi
log(iterator.next()) // Chidume

По сути, метод @@iterator возвращает итератор, используемый for...of, чтобы выполнить цикл через реализирующий объект для получения значений. Таким образом, если объект не обладает методом @@iterator и/или возвращает итератор, то оператор for...of не будет выполнен.

const nonIterable = //...
 for( let a of nonIterable) {
     // ...
 }

for( let a of nonIterable) {
               ^
TypeError: nonIterable is not iterable

Примеры итерируемых объектов:

  • String
  • Map
  • TypedArray
  • Array
  • Set
  • Generator

Обратите внимание, что объект не присутствует в списке. Объект не является итерируемым. Если использовать цикл через свойства объекта с помощью конструкции for…of:

let obj {
    firstname: "Nnamdi",
    surname: "Chidume"
}

for(const a of obj) {
    log(a)
}

Будет выдана ошибка:

for(const a of obj) {
               ^
TypeError: obj is not iterable

Есть способ проверить, является ли объект итерируемым:

const str = new String('Chidume');
log(typeof str[Symbol.iterator]);

function

Регистрируется function, которая показывает, что свойство @@iterator присутствует в строке. Попробуем использовать объект:

const obj = {
    surname: "Chidume"
}
log(typeof obj[Symbol.iterator]);

undefined

Ура! undefined означает отсутствие.

for…of: Array

Массив является итерируемым.

log(typeof new Array("Nnamdi", "Chidume")[Symbol.iterator]);
// function

Поэтому с ним можно использовать цикл for...of.

const arr = ["Chidume", "Nnamdi", "loves", "JS"]

for(const a of arr) {
    log(a)
}

// It logs:
// Chidume
// Nnamdi
// loves
// JS

const arr = new Array("Chidume", "Nnamdi", "loves", "JS")
for(const a of arr) {
    log(a)
}

// It logs:
// Chidume
// Nnamdi
// loves
// JS

for…of: String

Строка также является итерируемой.

const myname = "Chidume Nnamdi"

for(const a of myname) {
    log(a)
}

// It logs:
// C
// h
// i
// d
// u
// m
// e
// 
// N
// n
// a
// m
// d
// i

const str = new String("The Young")

for(const a of str) {
    log(a)
}

// It logs:
// T
// h
// e
// 
// Y
// o
// u
// n
// g

for…of: Map

const map = new Map([["surname", "Chidume"],["firstname","Nnamdi"]])

for(const a of map) {
    log(a)
}

// It logs:
// ["surname", "Chidume"]
// ["firstname","Nnamdi"]

for(const [key, value] of map) {
    log(`key: ${key}, value: ${value}`)
}
// It logs:
// key: surname, value: Chidume
// key: firstname, value: Nnamdi

for…of: Set

const set = new Set(["Chidume","Nnamdi"])

for(const a of set) {
    log(a)
}

// It logs:
// Chidume
// Nnamdi

for…of: TypedArray

const typedarray = new Uint8Array([0xe8, 0xb4, 0xf8, 0xaa]);

for (const a of typedarray) {
  log(a);
}

// It logs:
// 232
// 180
// 248
// 170

for…of: arguments

Являются ли аргументы итерируемыми? Проверим:

// testFunc.js
function testFunc(arg) {
    log(typeof arguments[Symbol.iterator])
}
testFunc()

$ node testFunc
function

Да, все получилось. Аргументы обладают типом IArguments, а класс, реализующий интерфейс IArguments, имеет свойство @@iterator, благодаря которому аргументы являются итерируемыми.

// testFunc.js
function testFunc(arg) {
    log(typeof arguments[Symbol.iterator])
    for(const a of arguments) {
        log(a)
    }
}
testFunc("Chidume")

// It:
// Chidume

for…of: Пользовательские итерируемые объекты

Можно создать пользовательский итерируемый объект, который может использоваться циклом for..of.

var obj = {}
obj[Symbol.iterator] = function() {
    var array = ["Chidume", "Nnamdi"]
    return {
        next: function() {
            let value = null
            if (this.index == 0) {
                value = array[this.index]
                this.index++
                    return { value, done: false }
            }
            if (this.index == 1) {
                value = array[this.index]
                this.index++
                    return { value, done: false }
            }
            if (this.index == 2) {
                return { done: true }
            }
        },
        index: 0
    }
};

Я создал объект obj и, чтобы сделать его итерируемым, назначил для него свойство @@iterator с помощью [Symbol.iterator]. Затем создал функцию для возврата итератора.

//...
return {
next: function() {...}
}
//...

Не забывайте, что итератор должен обладать функцией next().

Внутри функции next я реализовал значения, которые будут возвращены for..of в процессе итерации. Посмотрите на пример и убедитесь, что все это достаточно просто выполнить.

Протестируем объект obj:

// customIterableTest.js
//...
for (let a of obj) {
    log(a)
}

$ node customIterableTest
Chidume
Nnamdi

Как сделать Object и простые объекты итерируемыми

Простые объекты не являются итерируемыми, как и объекты из Object.

Однако этот момент можно обойти, добавив @@iterator к Object.prototype с пользовательским итератором.

Object.prototype[Symbol.iterator] = function() {
    let properties = Object.keys(this)
    let count = 0
    let isdone = false
    let next = () => {
        let value = this[properties[count]]
        if (count == properties.length) {
            isdone = true
        }
        count++
        return { done: isdone, value }
    }
    return { next }
}

Переменная properties содержит свойства объекта, полученного с помощью вызова Object.keys(). В функции next возвращается каждое значение из переменной properties и обновляется count, чтобы получить следующее значение из переменной properties, используя переменную count в качестве индекса. Когда count будет равен длине properties, устанавливаем значение true, чтобы остановить итерацию.

Тестирование с помощью Object:

let o = new Object()
o.s = "SK"
o.me = 'SKODA'
for (let a of o) {
    log(a)
}

SK
SKODA

Работает!!!

С простыми объектами:

let dd = {
    shit: 900,
    opp: 800
}

for (let a of dd) {
    log(a)
}

900
800

Та-дам!! 🙂

Стоить добавить этот способ в качестве полифилла, чтобы использовать for..of с любыми объектами в приложении.

Использование for…of с классами ES6

Можно использовать for..of для итерации по списку данных в экземпляре класса.

class Profiles {
    constructor(profiles) {
        this.profiles = profiles
    }
}

const profiles = new Profiles([
    {
        firstname: "Nnamdi",
        surname: "Chidume"
    },
    {
        firstname: "Philip",
        surname: "David"
    }
])

Класс Profiles обладает свойством profile, которое содержит массив пользователей. Возможно, потребуется отобразить эти данные в приложении с помощью for…of. Пробуем:

//...
for(const a of profiles) {
log(a)
}

Очевидно, for…of не сработает

for(const a of profiles) {
               ^

TypeError: profiles is not iterable

Вот несколько правил, чтобы сделать profiles итерируемым:

  • Объект должен иметь свойство @@iterator.
  • Функция @@iterator должна возвращать итератор.
  • iterator должен реализовывать функцию next().

Свойство @@iterator определяется с помощью константы [Symbol.iterator].

class Profiles {
constructor(profiles) {
this.profiles = profiles
}
[Symbol.iterator]() {
let props = this.profiles
let propsLen = this.profiles.length
let count = 0
return {
next: function() {
if (count < propsLen) {
return { value: props[count++], done: false }
}
if (count == propsLen) {
return { done: true }
}
}
}
}
}

Запускаем:

//...
for(const a of profiles) {
    log(a)
}

$ node profile.js
{ firstname: 'Nnamdi', surname: 'Chidume' }
{ firstname: 'Philip', surname: 'David' }

Свойство profiles отображено.

Асинхронный Итератор

В ECMAScript 2018 была введена новая конструкция, способная циклически повторять массив промисов. Эта новая конструкция выглядит как for-await-of, а новый символ — Symbol.asyncIterator.

Функция Symbol.asyncIterator в итерируемых объектах возвращает итератор, который возвращает промис.

const f = {
[Symbol.asyncIterator]() {
return new Promise(...)
}
}

Разница между [Symbol.iterator] и [Symbol.asyncIterator]заключается в том, что первый возвращает { value, done }, а второй возвращает Promise, который устанавливает { value, done }.

Объект f будет выглядеть так:

const f = {
[Symbol.asyncIterator]() {
return {
next: function() {
if (this.index == 0) {
this.index++
return new Promise(res => res({ value: 900, done: false }))
}
return new Promise(res => res({ value: 1900, done: true }))
},
index: 0
}
}
}

f — это асинхронный итератор. Он всегда возвращает промис, обладающий функцией resolve, которая возвращает значение на каждой итерации.

Чтобы выполнить итерацию через f, используем новую конструкцию for-await-of, вместо for..of:

// ...
async function fAsyncLoop(){
    for await (const _f of f) {
        log(_f)
    }
}
fAsyncLoop()

$ node fAsyncLoop.js
900

С помощью for-await-of можно также совершить цикл через массив промисов:

const arrayOfPromises = [
    new Promise(res => res("Nnamdi")),
    new Promise(res => res("Chidume"))
]

async function arrayOfPromisesLoop(){
    for await (const p of arrayOfPromises) {
        log(p)
    }
}
arrayOfPromisesLoop()

$ node arrayOfPromisesLoop.js
Nnamdi
Chidume

Заключение

В этом посте мы ознакомились с циклом for...of более подробно. Сначала мы узнали, что такое for..of, а затем рассмотрели итерируемые объекты и их параметры. Затем просмотрели полный список итерируемых объектов в JS и прошлись по каждому, проверив работает ли for...of с ними.

Конструкция for..of помогает избежать сложностей и большого количества логики, делая код более чистым и читаемым. Если вы еще не пробовали эту крутую конструкцию в качестве цикла, то думаю, самое время сделать это.


Перевод статьи Chidume NnamdiUnderstanding the For…of Loop In JavaScript