Java Script

Каждый знает хотя бы один вид цикла for. Это классика, и они есть почти в каждом языке. В JavaScript есть три вида циклов (или 4, если быть точным):

  • Классический цикл for
  • Пара for…of и for…in
  • И модный, функциональный .forEach

Далее я расскажу, чем именно они отличаются друг от друга, а также как и когда применять тот или иной вид цикла для достижения оптимальных результатов. Давайте разбираться.

Классический цикл for

Здесь всё ясно. Это классический цикл for, где вы определяете внутренний счётчик, задаёте условие прекращения цикла и изменение шага (обычно увеличивая или уменьшая счётчик).

Синтаксис:

for([counter definition];[breaking condition definition];[step definition]){
   //... ваш повторяющийся код
}

Я уверен, что вы прекрасно знаете эту конструкцию. Вот типичный пример:

for(let counter = 0; counter < 10; counter++) {
  console.log(counter)
}

Этот код прекрасно работает, но конструкция такого вида цикла может быть более гибкой, чем в этом примере. Вот как можно представить себе эту конструкцию:

for(
 [ВЫРАЖЕНИЕ, ИСПОЛНЯЕМОЕ ТОЛЬКО ОДИН РАЗ, ПРИ СТАРТЕ ЦИКЛА];
 [УСЛОВИЕ, КОТОРОЕ ПРОВЕРЯЕТСЯ НА КАЖДОМ ШАГЕ ЦИКЛА];
 [ВЫРАЖЕНИЕ, КОТОРОЕ ВЫПОЛНЯЕТСЯ НА КАЖДОМ ШАГЕ ЦИКЛА]
 )

Т.е. вы можете создавать циклы с более чем одним счётчиком, например. Или выполнять код на каждом шаге, который может даже не влиять на счётчики. Приведу несколько примеров.

Вот полностью валидный цикл:

for(let a = 0, b = 0; a < 10 && b < 100; a++, b+=10) {
   console.log(a, b)
}
/*
0 0
1 10
2 20
3 30
4 40
5 50
6 60
7 70
8 80
9 90
*/

Можно пойти ещё дальше и выйти за рамки типичных случаев, как в примере выше:

for(let a = 0, b = 0; a < 10 && b < 100; console.log("Your counters are at:", ++a, b+=2)){}
/*
Your counters are at: 1 2
Your counters are at: 2 4
Your counters are at: 3 6
Your counters are at: 4 8
Your counters are at: 5 10
Your counters are at: 6 12
Your counters are at: 7 14
Your counters are at: 8 16
Your counters are at: 9 18
Your counters are at: 10 20
*/

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

function isItDone(a) {
 console.log("fn called!")
 return a < 10
}

for(let a = 0; isItDone(a); a++) {
 console.log(a)
}
/*
fn called!
0
fn called!
1
fn called!
2
fn called!
3
fn called!
4
fn called!
5
fn called!
6
fn called!
7
fn called!
8
fn called!
9
fn called!
*/

Что делать с асинхронным кодом внутри классического цикла? Благодаря новой фиче: async/await, всё решается просто:

const fs = require("fs")

async function read(fname) {
    return new Promise( (resolve, reject) => {
        fs.readFile(fname, (err, content) => {
            if(err) return reject(err)
            resolve(content.toString())
        })
    })
}

(async () => {
    let files = ['file1.json', 'file2.json']

    for(let i = 0; i < files.length; i++) {
        let fcontent = await read(files[i])
        console.log(fcontent)
        console.log("-------")
    }
})()

Обратите внимание, как просто можно использовать цикл. Как будто нет никакой асинхронности. Благодаря async/await мы возвращаемся к зависимости от базовой конструкции, такой как цикл for, чтобы перебрать набор асинхронных инструкций.

Раньше мы использовали колбэки и промисы, чтобы достичь того же результата, поэтому логика была намного сложнее. Для ее упрощения появились специальные библиотеки, такие как async.js .

Кстати, небольшое примечание: цикл for в моём примере находится внутри IIFE просто потому, что инструкция await должна быть внутри асинхронной функции, чтобы не было проблем из-за Node. 

Пара for…of и for…in

Эта конструкция очень похожа на предыдущую, но в тоже время имеет свои особенности.

Цикл for…in обрабатывает несимвольные, перечисляемые свойства объекта (ключевое слово здесь — «объект», потому что почти всё в JavaScript является объектом). Этот цикл особенно полезен, когда вы используете пользовательский объект в качестве хэш-карты или словаря (очень распространённая практика).

Обратите внимание, что итерация выполняется в произвольном порядке, поэтому если вам нужен «правильный порядок» убедитесь, что вы контролируете этот процесс.

let myMap {
  uno: 1,
  dos: 2,
  tres: 3
}
for(let key in myMap) {
  console.log(key, "=", myMap[key]);
}
/*
uno = 1
dos = 2
tres = 3
*/

Выглядит довольно просто. Но помните, что почти всё в JavaScript — это объекты, поэтому можно легко перепутать, когда вам нужен цикл for… in, а когда for… of. Например, если вы хотите перебрать каждый символ в строке (которая является объектом), вот что произойдёт, если вы используете for… in:

for(let k in "Hello World!") {
   console.log(k)
}
/*
0
1
2
3
4
5
6
7
8
9
10
11
*/

Вместо того, чтобы перебирать каждую букву строки, цикл перебирал каждое свойство, и, как видите, эта структура (для строкового типа), очень похожа на массив. И в этом есть смысл. Если задать "Hello World!", цикл не сработает и вернет букву «e».

Если вы хотите перебрать каждый символ, то нужно использовать вариант: for…of

for(let char of "Hello World!") {
  console.log(char)
}
/*
H
e
l
l
o
 
W
o
r
l
d
!
*/

Вот теперь в этом есть смысл. Та же задача, но с for…of вы получаете доступ к значениям итерируемого объекта (итерируемыми могут быть строки, массивы, карты, наборы и структуры подобные массивам, такие как arguments и NodeList), конечно, если вы определяете их как итерируемые.

В приведённом выше примере нет прямого способа получить текущий индекс для цикла, если только вы не определите его вне цикла и не будете обновлять его на каждом шаге. Индекс и значение можно получить, если применить метод entries для массивов, например:

let myArr = ["hello", "world"]
for([idx, value] of myArr.entries()) {
    console.log(idx, '=', value)
}
/*
0 '=' 'hello'
1 '=' 'world'
*/

А что здесь с асинхронным кодом? Совершенно то же самое.

const fs = require("fs")

async function read(fname) {
    return new Promise( (resolve, reject) => {
        fs.readFile(fname, (err, content) => {
            if(err) return reject(err)
            resolve(content.toString())
        })
    })
}



(async () => {
    let files = ['file2.json', 'file2.json']

    for(fname of files) {
        let fcontent = await read(fname)
        console.log(fcontent)
        console.log("-------")
    }

    for(idx in files) {
        let fcontent = await read(files[idx])
        console.log(fcontent)
        console.log("-------")
    }
})()

Оба цикла ведут себя одинаково с конструкцией await, что позволяет писать более простой и чистый код.

Циклы .forEach

Мне очень нравится работать с этой конструкцией просто потому, что я большой поклонник декларативного синтаксиса и декларативного способа написания вообще. Вышеприведённые виды цикла отлично работают и имеют свои преимущества в определённых случаях, но они также очень императивны. Т.е. мы должны писать то, что должно произойти с данными, а не то, что мы хотим с ними сделать.

Итак, оставим философские размышления. Метод .forEach — это ещё один вид цикла for. Он является частью объекта массива и предназначен для получения функции и дополнительного, необязательного параметра для повторного определения контекста этой функции при её выполнении.

Для каждого элемента внутри массива функция будет выполнена, и получит три аргумента (да, всё правильно, три аргумента, а не один, как обычно). Вот эти аргументы:

  1. Элемент, который обрабатывается в текущий момент.
  2. Индекс элемента (это уже упрощает задачу, которую мы пытались решить с помощью цикла for…of).
  3. Сам обрабатываемый массив. На случай если вам нужно что-то с ним сделать.

Небольшой пример:

a = ["hello", "world"]

a.forEach ( (elem, idx, arr) => {
   console.log(elem, "at: ", idx, "inside: ", arr)
})
/*
hello at:  0 inside:  [ 'hello', 'world' ]
world at:  1 inside:  [ 'hello', 'world' ]
*/

Быстро и просто. Посмотрите, как легко можно работать с атрибутами внутри функции. Ниже пример того, как можно использовать второй дополнительный параметр метода forEach:

class Person {
    constructor(name)  {
        this.name = name
    }
}

function greet(person) {
    console.log(this.greeting.replace("$", person.name))
}

let english = {
    greeting: "Hello there, $"
}
let spanish = {
    greeting: "Hola $, ¿cómo estás?"
}

let people = [new Person("Fernando"), new Person("Federico"), new Person("Felipe")]


people.forEach( greet, english)
people.forEach( greet, spanish)

Перезаписывая контекст вызываемой функции greet, я могу изменить её поведение, не изменяя её код.

В завершении хочу показать, что этот метод тоже работает с асинхронным кодом. Вот пример:

const fs = require("fs")

async function read(fname) {
    return new Promise( (resolve, reject) => {
        fs.readFile(fname, (err, content) => {
            if(err) return reject(err)
            resolve(content.toString())
        })
    })
}

let files = ['file1.json', 'file2.json']

files.forEach( async fname => {
    let fcontent = await read(fname)
    console.log(fcontent)
    console.log("-------")
})

Обратите внимание, что мне больше не нужен IIFE, потому что я объявляю колбэк как async.

Заключение

Это всё, что я хотел рассказать о циклах for в JavaScript. Надеюсь, это дало вам более чёткое понимание, как они работают и какой из них выбрать для конкретных задач.

Читайте также:


Перевод статьи Fernando Doglio: 3 Flavors of the For Loop in JavaScript and When to Use Them