В JavaScript много операторов цикла:
- оператор
while
- оператор
do...while
- оператор
for
- оператор
for...in
- оператор
for...of
Их основная функция: повторять действия до тех пор, пока не будет выполнено определенное условие.
В этой статье мы узнаем, как работает оператор for...of
и, где его следует использовать при написании кода в приложениях JS.
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 Nnamdi: Understanding the For…of Loop In JavaScript