[Перевод] Руководство по JavaScript, часть 5: массивы и циклы
Сегодня, в пятой части перевода курса по JavaScript, мы поговорим о массивах и циклах. Массивы используются в ходе решения множества задач. Часто с массивами работают, используя циклы.
→ Часть 1: первая программа, особенности языка, стандарты
→ Часть 2: стиль кода и структура программ
→ Часть 3: переменные, типы данных, выражения, объекты
→ Часть 4: функции
Массивы
Массивы, объекты типа Array
, развиваются вместе с остальными механизмами языка. Они представляют собой списки пронумерованных значений.
Первый элемент массива имеет индекс (ключ) 0, такой подход используется во многих языках программирования.
В этом разделе мы рассмотрим современные методы работы с массивами.
▍Инициализация массивов
Вот несколько способов инициализации массивов.
const a = []
const a = [1, 2, 3]
const a = Array.of(1, 2, 3)
const a = Array(6).fill(1) //инициализация каждого элемента массива, состоящего из 6 элементов, числом 1
Для того чтобы получить доступ к отдельному элементу массива, используют конструкцию, состоящую из квадратных скобок, в которых содержится индекс элемента массива. Элементы массивов можно как считывать, так и записывать.
const a = [1, 2, 3]
console.log(a) //[ 1, 2, 3 ]
const first = a[0]
console.log(first) //1
a[0] = 4
console.log(a) //[ 4, 2, 3 ]
Конструктор Array
для объявления массивов использовать не рекомендуется.
const a = new Array() //не рекомендуется
const a = new Array(1, 2, 3) //не рекомендуется
Этот способ следует использовать лишь при объявлении типизированных массивов.
▍Получение длины массива
Для того чтобы узнать длину массива, нужно обратиться к его свойству length
.
const l = a.length
▍Проверка массива с использованием метода every ()
Метод массивов every()
можно использовать для организации проверки всех их элементов с использованием некоего условия. Если все элементы массива соответствуют условию, функция возвратит true
, в противном случае она возвратит false
.
Этому методу передаётся функция, принимающая аргументы currentValue
(текущий элемент массива), index
(индекс текущего элемента массива) и array
(сам массив). Он может принимать и необязательное значение, используемое в качестве this
при выполнении переданной ему функции.
Например, проверим, превышают ли значения всех элементов массива число 10.
const a = [11, 12, 13]
const b = [5, 6, 25]
const test = el => el > 10
console.log(a.every(test)) //true
console.log(b.every(test)) //false
Здесь нас, в функции test()
, интересует лишь первый передаваемый ей аргумент, поэтому мы объявляем её, указывая лишь параметр el
, в который и попадёт соответствующее значение.
▍Проверка массива с использованием метода some ()
Этот метод очень похож на метод every()
, но он возвращает true
, если хотя бы один из элементов массива удовлетворяет условию, заданному переданной ему функцией.
▍Создание массива на основе существующего массива с использованием метода map ()
Метод массивов map()
позволяет перебирать массивы, применяя к каждому их элементу, переданную этому методу, функцию, преобразующую элемент, и создавать из полученных значений новые массивы. Вот, например, как получить новый массив, являющийся результатом умножения всех элементов исходного массива на 2.
const a = [1, 2, 3]
const double = el => el * 2
const doubleA = a.map(double)
console.log(a) //[ 1, 2, 3 ]
console.log(doubleA) //[ 2, 4, 6 ]
▍Фильтрация массива с помощью метода filter ()
Метод filter()
похож на метод map()
, но он позволяет создавать новые массивы, содержащие лишь те элементы исходных массивов, которые удовлетворяют условию, задаваемому передаваемой методу filter()
функцией.
▍Метод reduce ()
Метод reduce()
позволяет применить заданную функцию к аккумулятору и к каждому значению массива, сведя массив к единственному значению (это значение может иметь как примитивный, так и объектный тип). Этот метод принимает функцию, выполняющую преобразования, и необязательное начальное значение аккумулятора. Рассмотрим пример.
const a = [1, 2, 3, 4].reduce((accumulator, currentValue, currentIndex, array) => {
return accumulator * currentValue
}, 1)
console.log(a) //24
//итерация 1: 1 * 1 = 1
//итерация 2: 1 * 2 = 2
//итерация 3: 2 * 3 = 6
//итерация 4: 6 * 4 = 24
Здесь мы ищем произведение всех элементов массива, описанного с помощью литерала, задавая в качестве начального значения аккумулятора 1.
▍Перебор массива с помощью метода forEach ()
Метод массивов forEach()
можно использовать для перебора значений массивов и для выполнения над ними неких действий, задаваемых передаваемой методу функцией. Например, выведем, по одному, элементы массива в консоль.
const a = [1, 2, 3]
a.forEach(el => console.log(el))
//1
//2
//3
Если при переборе массива нужно остановить или прервать цикл, то при использовании forEach()
придётся выбрасывать исключение. Поэтому если в ходе решения некоей задачи может понадобиться прерывание цикла, лучше всего выбрать какой-нибудь другой способ перебора элементов массива.
▍Перебор массива с использованием оператора for…of
Оператор for...of
появился в стандарте ES6. Он позволяет перебирать итерируемые объекты (в том числе — массивы). Вот как им пользоваться.
const a = [1, 2, 3]
for (let v of a) {
console.log(v)
}
//1
//2
//3
На каждой итерации цикла в переменную v
попадает очередной элемент массива a
.
▍Перебор массива с использованием оператора for
Оператор for
позволяет организовывать циклы, которые, в частности, можно использовать и для перебора (или инициализации) массивов, обращаясь к их элементам по индексам. Обычно индекс очередного элемента получают, пользуясь счётчиком цикла.
const a = [1, 2, 3]
for (let i = 0; i < a.length; i += 1) {
console.log(a[i])
}
//1
//2
//3
Если, в ходе выполнения цикла, нужно пропустить его итерацию, можно воспользоваться командой continue
. Для досрочного завершения цикла можно воспользоваться командой break
. Если в цикле, например, расположенном в некоей функции, использовать команду return
, выполнение цикла и функции завершится, а возвращённое с помощью return
значение попадёт туда, откуда была вызвана функция.
▍Метод @@iterator
Этот метод появился в стандарте ES6. Он позволяет получать так называемый «итератор объекта» — объект, который в данном случае позволяет организовывать перебор элементов массива. Итератор массива можно получить, воспользовавшись символом (такие символы называют «известными символами») Symbol.iterator
. После получения итератора можно обращаться к его методу next()
, который, при каждом его вызове, возвращает структуру данных, содержащую очередной элемент массива.
const a = [1, 2, 3]
let it = a[Symbol.iterator]()
console.log(it.next().value) //1
console.log(it.next().value) //2
console.log(it.next().value) //3
Если вызвать метод next()
после того, как будет достигнут последний элемент массива, он возвратит, в качестве значения элемента, undefined
. Объект, возвращаемый методом next()
, содержит свойства value
и done
. Свойство done
принимает значение true
до тех пор, пока не будет достигнут последний элемент массива. В нашем случае, если вызвать it.next()
в четвёртый раз, он возвратит объект { value: undefined, done: true }
, в то время как при трёх предыдущих вызовах этот объект имел вид { value: значение, done: false }
.
Метод массивов entries()
возвращает итератор, который позволяет перебирать пары ключ-значение массива.
const a = [1, 2, 3]
let it = a.entries()
console.log(it.next().value) //[0, 1]
console.log(it.next().value) //[1, 2]
console.log(it.next().value) //[2, 3]
Метод keys()
позволяет перебирать ключи массива.
const a = [1, 2, 3]
let it = a.keys()
console.log(it.next().value) //0
console.log(it.next().value) //1
console.log(it.next().value) //2
▍Добавление элементов в конец массива
Для добавления элементов в конец массива используют метод push()
.
a.push(4)
▍Добавление элементов в начало массива
Для добавления элементов в начало массива используют метод unshift()
.
a.unshift(0)
a.unshift(-2, -1)
▍Удаление элементов массива
Удалить элемент из конца массива, одновременно возвратив этот элемент, можно с помощью метода pop()
.
a.pop()
Аналогичным образом, с помощью метода shift()
, можно удалить элемент из начала массива.
a.shift()
То же самое, но уже с указанием позиции удаления элементов и их количества, делается с помощью метода splice()
.
a.splice(0, 2) // удаляет и возвращает 2 элемента из начала массива
a.splice(3, 2) // удаляет и возвращает 2 элемента, начиная с индекса 3
▍Удаление элементов массива и вставка вместо них других элементов
Для того чтобы, воспользовавшись одной операцией, удалить некие элементы массива и вставить вместо них другие элементы, используется уже знакомый вам метод splice()
.
Например, здесь мы удаляем 3 элемента массива начиная с индекса 2, после чего в то же место добавляем два других элемента:
const a = [1, 2, 3, 4, 5, 6]
a.splice(2, 3, 'a', 'b')
console.log(a) //[ 1, 2, 'a', 'b', 6 ]
▍Объединение нескольких массивов
Для объединения нескольких массивов можно воспользоваться методом concat()
, возвращающим новый массив.
const a = [1, 2]
const b = [3, 4]
const c = a.concat(b)
console.log(c) //[ 1, 2, 3, 4 ]
▍Поиск элементов в массиве
В стандарте ES5 появился метод indexOf()
, который возвращает индекс первого вхождения искомого элемента массива. Если элемент в массиве найти не удаётся — возвращается -1
.
const a = [1, 2, 3, 4, 5, 6, 7, 5, 8]
console.log(a.indexOf(5)) //4
console.log(a.indexOf(23)) //-1
Метод lastIndexOf()
возвращает индекс последнего вхождения элемента в массив, или, если элемент не найден, -1
.
const a = [1, 2, 3, 4, 5, 6, 7, 5, 8]
console.log(a.lastIndexOf(5)) //7
console.log(a.lastIndexOf(23)) //-1
В ES6 появился метод массивов find()
, который выполняет поиск по массиву с использованием передаваемой ему функции. Если функция возвращает true
, метод возвращает значение первого найденного элемента. Если элемент найти не удаётся, функция возвратит undefined
.
Выглядеть его использование может следующим образом.
a.find(x => x.id === my_id)
Здесь в массиве, содержащем объекты, осуществляется поиск элемента, свойство id
которого равняется заданному.
Метод findIndex()
похож на find()
, но он возвращает индекс найденного элемента или undefined
.
В ES7 появился метод includes()
, который позволяет проверить наличие некоего элемента в массиве. Он возвращает true
или false
, найдя или не найдя интересующий программиста элемент.
a.includes(value)
С помощью этого метода можно проверять на наличие некоего элемента не весь массив, а лишь некоторую его часть, начинающуюся с заданного при вызове этого метода индекса. Индекс задаётся с помощью второго, необязательного, параметра этого метода.
a.includes(value, i)
▍Получение фрагмента массива
Для того чтобы получить копию некоего фрагмента массива в виде нового массива, можно воспользоваться методом slice()
. Если этот метод вызывается без аргументов, то возвращённый массив окажется полной копией исходного. Он принимает два необязательных параметра. Первый задаёт начальный индекс фрагмента, второй — конечный. Если конечный индекс не задан, то массив копируется от заданного начального индекса до конца.
const a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(a.slice(4)) //[ 5, 6, 7, 8, 9 ]
console.log(a.slice(3,7)) //[ 4, 5, 6, 7 ]
▍Сортировка массива
Для организации сортировки элементов массива в алфавитном порядке (0-9A-Za-z
) используется метод sort()
без передачи ему аргументов.
const a = [1, 2, 3, 10, 11]
a.sort()
console.log(a) //[ 1, 10, 11, 2, 3 ]
const b = [1, 'a', 'Z', 3, 2, 11]
b.sort()
console.log(b) //[ 1, 11, 2, 3, 'Z', 'a' ]
Этому методу можно передать функцию, задающую порядок сортировки. Функция принимает, для сравнения двух элементов, параметры a
и b
. Она возвращает отрицательное число в том случае, если a
меньше b
по какому-либо критерию, 0 — если они равны, и положительное число — если a
больше b
. При написании подобной функции для сортировки числовых массивов она может возвратить результат вычитания a
и b
. Так, возврат результата вычисления выражения a - b
означает сортировку массива по возрастанию, возврат результата вычисления выражения b - a
даст сортировку массива по убыванию.
const a = [1, 10, 3, 2, 11]
console.log(a.sort((a, b) => a - b)) //[ 1, 2, 3, 10, 11 ]
console.log(a.sort((a, b) => b - a)) //[ 11, 10, 3, 2, 1 ]
Для того чтобы обратить порядок следования элементов массива можно воспользоваться методом reverse()
. Он, так же, как и sort()
, модифицирует массив для которого вызывается.
▍Получение строкового представления массива
Для получения строкового представления массива можно воспользоваться его методом toString()
.
a.toString()
Похожий результат даёт метод join()
, вызванный без аргументов.
a.join()
Ему, в качестве аргумента, можно передать разделитель элементов.
const a = [1, 10, 3, 2, 11]
console.log(a.toString()) //1,10,3,2,11
console.log(a.join()) //1,10,3,2,11
console.log(a.join(', ')) //1, 10, 3, 2, 11
▍Создание копий массивов
Для создания копии массива путём копирования в новый массив значений исходного массива можно воспользоваться методом Array.from()
. Он подходит и для создания массивов из массивоподобных объектов (из строк, например).
const a = 'a string'
const b = Array.from(a)
console.log(b) //[ 'a', ' ', 's', 't', 'r', 'i', 'n', 'g' ]
Метод Array.of()
тоже можно использовать для копирования массивов, а также для «сборки» массивов из различных элементов. Например, для копирования элементов одного массива в другой можно воспользоваться следующей конструкцией.
const a = [1, 10, 3, 2, 11]
const b = Array.of(...a)
console.log(b) // [ 1, 10, 3, 2, 11 ]
Для копирования элементов массива в некое место самого этого массива используется метод copyWithin()
. Его первый аргумент задаёт начальный индекс целевой позиции, второй — начальный индекс позиции источника элементов, а третий параметр, необязательный, указывает конечный индекс позиции источника элементов. Если его не указать, в указанное место массива будет скопировано всё, начиная от начального индекса позиции источника до конца массива.
const a = [1, 2, 3, 4, 5]
a.copyWithin(0, 2)
console.log(a) //[ 3, 4, 5, 4, 5 ]
Циклы
Выше, говоря о массивах, мы уже сталкивались с некоторыми способами организации циклов. Однако циклы в JavaScript используются не только для работы с массивами, да и рассмотрели мы далеко не все их виды. Поэтому сейчас мы уделим некоторое время рассмотрению разных способов организации циклов в JavaScript и поговорим об их особенностях.
▍Цикл for
Рассмотрим пример применения этого цикла.
const list = ['a', 'b', 'c']
for (let i = 0; i < list.length; i++) {
console.log(list[i]) //значения, хранящиеся в элементах циклов
console.log(i) //индексы
}
Как уже было сказано, прерывать выполнение такого цикла можно, используя команду break
, а пропускать текущую итерацию и переходить сразу к следующей можно с помощью команды continue
.
▍Цикл forEach
Этот цикл мы тоже обсуждали. Приведём пример перебора массива с его помощью.
const list = ['a', 'b', 'c']
list.forEach((item, index) => {
console.log(item) //значение
console.log(index) //индекс
})
//если индексы элементов нас не интересуют, можно обойтись и без них
list.forEach(item => console.log(item))
Напомним, что для прерывания такого цикла надо выбрасывать исключение, то есть, если при использовании цикла может понадобиться прервать его, лучше выбрать какой-нибудь другой цикл.
▍Цикл do…while
Это — так называемый «цикл с постусловием». Такой цикл будет выполнен как минимум один раз до проверки условия завершения цикла.
const list = ['a', 'b', 'c']
let i = 0
do {
console.log(list[i]) //значение
console.log(i) //индекс
i = i + 1
} while (i < list.length)
Его можно прерывать с использованием команды break
, можно переходить на его следующую итерацию командой continue
.
▍Цикл while
Это — так называемый «цикл с предусловием». Если, на входе в цикл, условие продолжения цикла ложно, он не будет выполнен ни одного раза.
const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
console.log(list[i]) //значение
console.log(i) //индекс
i = i + 1
}
▍Цикл for…in
Этот цикл позволяет перебирать все перечислимые свойства объекта по их именам.
let object = {a: 1, b: 2, c: 'three'}
for (let property in object) {
console.log(property) //имя свойства
console.log(object[property]) //значение свойства
}
▍Цикл for…of
Цикл for...of
совмещает в себе удобство цикла forEach
и возможность прерывать его работу штатными средствами.
//перебор значений
for (const value of ['a', 'b', 'c']) {
console.log(value) //значение
}
//перебор значений и получение индексов с помощью `entries()`
for (const [index, value] of ['a', 'b', 'c'].entries()) {
console.log(index) //индекс
console.log(value) //значение
}
Обратите внимание на то, что здесь, в заголовке цикла, используется ключевое слово const
, а не, как можно было бы ожидать, let
. Если внутри блока цикла переменные не нужно переназначать, то const
нам вполне подходит.
Если сравнить циклы for...in
и for...of
, то окажется, что for...in
перебирает имена свойств, а for...of
— значения свойств.
Циклы и области видимости
С циклами и с областями видимости переменных связана одна особенность JavaScript, которая может доставить разработчику некоторые проблемы. Для того чтобы с этими проблемами разобраться, поговорим о циклах, об областях видимости, и о ключевых словах var
и let
.
Рассмотрим пример.
const operations = []
for (var i = 0; i < 5; i++) {
operations.push(() => {
console.log(i)
})
}
for (const operation of operations) {
operation()
}
В цикле производится 5 итераций, на каждой из которых в массив operations
добавляется новая функция. Эта функция выводит в консоль значение счётчика цикла — i
. После того, как функции добавлены в массив, мы этот массив перебираем и вызываем функции, являющиеся его элементами.
Выполняя подобный код можно ожидать результата, показанного ниже.
0
1
2
3
4
Но на самом деле он выводит следующее.
5
5
5
5
5
Почему это так? Всё дело в том, что в качестве счётчика цикла мы используем переменную, объявленную с использованием ключевого слова var
.
Так как объявления подобных переменных поднимаются в верхнюю часть области видимости, вышеприведённый код аналогичен следующему.
var i;
const operations = []
for (i = 0; i < 5; i++) {
operations.push(() => {
console.log(i)
})
}
for (const operation of operations) {
operation()
}
В результате оказывается, что в цикле for...of
, в котором мы перебираем массив, переменная i
всё ещё видна, она равна 5, в результате, ссылаясь на i
во всех функциях, мы выводим число 5.
Как изменить поведение программы таким образом, чтобы она делала бы то, что от неё ожидается?
Самое простое решение этой проблемы заключается в использовании ключевого слова let
. Оно, как мы уже говорили, появилось в ES6, его использование позволяет избавиться от некоторых странностей, характерных для var
.
В частности, в вышеприведённом примере достаточно изменить var
на let
и всё заработает так, как нужно.
const operations = []
for (let i = 0; i < 5; i++) {
operations.push(() => {
console.log(i)
})
}
for (const operation of operations) {
operation()
}
Теперь на каждой итерации цикла каждая функция, добавленная в массив operations
, получает собственную копию i
. Помните о том, что в данной ситуации нельзя использовать ключевое слово const
, так как значение i
в цикле меняется.
Ещё один способ решения этой проблемы, который часто применялся до появления стандарта ES6, когда ключевого слова let
ещё не было, заключается в использовании IIFE.
При таком подходе значение i
сохраняется в замыкании, а в массив попадает функция, возвращаемая IIFE и имеющая доступ к замыканию. Эту функцию можно выполнить тогда, когда в ней возникнет необходимость. Вот как это выглядит.
const operations = []
for (var i = 0; i < 5; i++) {
operations.push(((j) => {
return () => console.log(j)
})(i))
}
for (const operation of operations) {
operation()
}
Итоги
Сегодня мы поговорили о массивах и о циклах в JavaScript. Тема нашего следующего материала — обработка исключений, особенности использования точки с запятой и шаблонные литералы.
Уважаемые читатели! Какими методами для работы с массивами в JavaScript вы пользуетесь чаще всего?