X

Что нужно знать про массивы в JavaScript

Когда дело доходит до прохода по массиву, поиску элементов и так далее, вероятнее всего, для этого есть соответствующий метод массива. Однако часть методов остаётся в тени, поэтому в этой статье поговорим о полезных методах массивов в JavaScript.

Пример использования метода reduce для «сглаживания» массива

Основа основ

Есть 4 вещи, которые вы должны знать при работе с массивами — это map, filter, reduce и spread-оператор. Они являются мощным базисом.

map

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

Метод map() принимает всего один параметр — функцию, которая вызывается при каждой итерации по массиву. Метод возвращает новый массив, а не изменяет существующий.

const numbers = [1, 2, 3, 4]
const numbersPlusOne = numbers.map(n => n + 1) 
console.log(numbersPlusOne) // [2, 3, 4, 5]

Подобным образом можно получить массив, содержащий в себе только одно свойство объекта:

const allActivities = [
 { title: 'My activity', coordinates: [ 50.123, 3.291] },
 { title: 'Another activity', coordinates: [ 1.238, 4.292] }
]

const allCoordinates = allActivities.map(activity => activity.coordinates)
console.log(allCoordinates) // [[ 50.123, 3.291], [ 1.238, 4.292]]

Поэтому, если нужно поэлементно трансформировать массив в новый — используйте map().

filter

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

Как и map(), метод filter() принимает только один параметр — функцию, которая вызывается при каждой итерации. Функция должна возвращать булево значение:

  • true — элемент остаётся в новом массиве,
  • false — элемент не остаётся в новом массиве.

После этого вы получаете отфильтрованный массив с нужными вам элементами.

К примеру, сохраним только нечётные числа в массиве:

const numbers = [1, 2, 3, 4, 5, 6]
const oddNumbers = numbers.filter(n => n % 2 !== 0)
console.log(oddNumbers) // [1, 3, 5]

Или же можно удалять строго определённые элементы массива:

const participants = [
 { id: 'a3f47', username: 'john' },
 { id: 'fek28', username: 'mary' },
 { id: 'n3j44', username: 'sam' },
]

function removeParticipant(participants, id) {
 return participants.filter(participant => participant.id !== id)
}

console.log(removeParticipant(participants, 'a3f47')) //  [{ id: 'fek28', username: 'mary' }, { id: 'n3j44', username: 'sam' }];

reduce

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

Метод reduce() предназначен для комбинации значений массива в одно значение. Метод принимает два параметра. Первый из них — callback-функция (reducer), второй — первичное значение, которое является необязательным и по-умолчанию является первым элементом массива. Callback-функция принимает 4 аргумента:

  • accumulator (он хранит в себе промежуточный результат итераций),
  • текущее значение массива,
  • текущий index,
  • сам массив.

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

Поначалу это может звучать сложно, но на примерах всё разъяснится. Вот самый простой из них:

const numbers = [37, 12, 28, 4, 9]
const total = numbers.reduce((total, n) => total + n)
console.log(total) // 90

При первой итерации переменная total (промежуточный результат) принимает значение 37. Возвращаемое значение равно (37 + n), где n равняется 12, т. е. значение равно 49. При второй итерации промежуточный результат равен 49 и к нему прибавляется 28. Теперь total равен 77. И так далее.

Метод reduce() настолько хорош, что с его помощью можно создавать остальные методы массива, например map() или filter():

const map = (arr, fn) => {
 return arr.reduce((mappedArr, element) => {
   return [...mappedArr, fn(element)]
 }, [])
}

console.log(map([1, 2, 3, 4], n => n + 1)) // [2, 3, 4, 5]

const filter = (arr, fn) => {
 return arr.reduce((filteredArr, element) => {
   return fn(element) ? [...filteredArr] : [...filteredArr, element]
 }, [])
}

console.log(filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0)) // [1, 3, 5]

В случае map() запускается функция, результат которой добавляется в конец accumulator’а с помощью spread-оператора. В filter() почти то же самое, за исключением того, что на каждом элементе запускается filter-функция. Если эта функция возвращает true, то возвращается предыдущий массив (промежуточное значение), иначе элемент добавляется в конец массива.

Вот более сложный пример: функция, которая «сглаживает» массив рода [1, 2, 3, [4, [[[5, [6, 7]]]], 8]] в одномерный массив [1, 2, 3, 4, 5, 6, 7, 8].

function flatDeep(arr) {
 return arr.reduce((flattenArray, element) => {
   return Array.isArray(element) ? [...flattenArray, ...flatDeep(element)] : [...flattenArray, element]
 }, [])
}

console.log(flatDeep([1, 2, 3, [4, [[[5, [6, 7]]]], 8]])) // [1, 2, 3, 4, 5, 6, 7, 8]

Принцип работы схож с map(), только с щепоткой рекурсии.

Spread оператор (стандарт ES2015)

Несмотря на то что оператор не является методом, с его помощью можно добиться многого при работе с массивами. Например, можно делать копии массивов или же объединять несколько массивов в один.

const numbers = [1, 2, 3]
const numbersCopy = [...numbers]

console.log(numbersCopy) // [1, 2, 3]

const otherNumbers = [4, 5, 6]
const numbersConcatenated = [...numbers, ...otherNumbers]

console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]

Обратите внимание, что этот оператор создаёт поверхностную копию исходного массива. Но что значит?

При поверхностном копировании элементы массива дублируются минимальным образом. Если, скажем, массив состоит только из примитивов (числа, строки, булевы значения), то это не вызовет никаких проблем. Но нельзя сказать то же самое о массиве объектов. При поверхностном копировании объектов в массиве созданные (продублированные) элементы ссылаются на оригиналы. Следовательно, если вы попытаетесь изменить объект в массиве, то изменения будут отражаться и на всех продублированных с него объектах (так же и в обратную сторону).

const arr = ['foo', 42, { name: 'Thomas' }]
let copy = [...arr]

copy[0] = 'bar'

console.log(arr) // Никаких изменений: ["foo", 42, { name: "Thomas" }]
console.log(copy) // ["bar", 42, { name: "Thomas" }]

copy[2].name = 'Hello'

console.log(arr) // /!\ ИЗМЕНЕНИЯ ["foo", 42, { name: "Hello" }]
console.log(copy) // ["bar", 42, { name: "Hello" }]

Поэтому если вам нужно сделать полную копию массива, содержащего объекты, то можно воспользоваться функцией cloneDeep из библиотеки lodash.

Углубляемся

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

includes (стандарт ES2016)

Часто ли вы использовали indexOf(), чтобы узнать о наличии чего-либо в массиве? Ужасный способ, не правда ли? К счастью, существует метод includes(). Он принимает всего один параметр — искомый элемент — и возвращает true/false в зависимости от результата поиска.

const sports = ['football', 'archery', 'judo']
const hasFootball = sports.includes('football')
console.log(hasFootball) // true

concat

Этот метод объединяет два или более массивов.

const numbers = [1, 2, 3]
const otherNumbers = [4, 5, 6]

const numbersConcatenated = numbers.concat(otherNumbers)

console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]

// Вы можете объединять любое количество массивов
function concatAll(arr, ...arrays) {
 return arr.concat(...arrays)
}

console.log(concatAll([1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12])) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

forEach

Этот метод нужен, когда вам необходимо выполнить что-либо для каждого элемента массива. Метод принимает функцию в качестве параметра. Сама функция принимает 3 параметра: текущее значение, индекс и сам массив:

const numbers = [1, 2, 3, 4, 5]
numbers.forEach(console.log)
// 1 0 [ 1, 2, 3 ]
// 2 1 [ 1, 2, 3 ]
// 3 2 [ 1, 2, 3 ]

indexOf

Этот метод возвращает индекс первого вхождения элемента в массиве. indexOf() так же часто используется в качестве проверки на наличие определённого элемента в массиве, хотя делать так не рекомендуется (ведь существует includes()).

const sports = ['football', 'archery', 'judo']
const judoIndex = sports.indexOf('judo')
console.log(judoIndex) // 2

find

Метод find() идентичен методу filter(). Ему тоже нужно передавать функцию, которая проверяет каждый элемент массива. Но в отличие от filter(), метод find() прекращает поиск, когда находит первый элемент, удовлетворяющий условию проверяющей функции.

const users = [
 { id: 'af35', name: 'john' },
 { id: '6gbe', name: 'mary' },
 { id: '932j', name: 'gary' },
]

const user = users.find(user => user.id === '6gbe')

console.log(user) // { id: '6gbe', name: 'mary' }

Используйте filter(), чтобы пройтись по всему массиву, а find() — чтобы найти уникальный элемент в нём.

findIndex

Метод полностью идентичен предыдущему методу find(), за исключением того, что findIndex() возвращает индекс конкретного искомого элемента.

const users = [
 { id: 'af35', name: 'john' },
 { id: '6gbe', name: 'mary' },
 { id: '932j', name: 'gary' },
]

const user = users.findIndex(user => user.id === '6gbe')

console.log(user) // 1

Возможно вы скажете: «Эй! Этот метод же делает тоже самое, что и indexOf()!».

Не совсем.

Передаваемый параметр в indexOf() — это просто примитив (число, строка, булево значение, null, undefined или просто символ), в то время как параметр в findIndex() — это callback-функция.

Поэтому если нужно найти индекс элемента в массиве примитивов, используйте indexOf(). В других же случаях (массивы объектов) берите findIndex().

slice

Метод используется в тех случаях, когда необходимо извлечь подмассив из массива. Однако с ним нужно быть осторожным. Как и spread-оператор, этот метод возвращает поверхностную копию подмассива.

Как говорилось в начале статьи, во многих случаях циклы можно заменить.

Допустим, через API вы получили какое-то количество сообщений, но хотите отобразить только 5 из них. Ниже приведены 2 способа: первый — с использованием цикла, второй — с использованием slice.

// «Традиционный» способ реализации:
// Определить количество сообщений для использования в цикле

const nbMessages = messages.length < 5 ? messages.length : 5
let messagesToShow = []
for (let i = 0; i < nbMessages; i++) {
 messagesToShow.push(posts[i])
}

// Даже если массив содержит меньше 5 элементов,
// slice вернёт поверхностную копию всего массива
const messagesToShow = messages.slice(0, 5)

some

Используйте этот метод чтобы узнать, удовлетворяет ли условию хотя бы один из элементов массива. Как и map(), fitler() и find(), метод some принимает callback-функцию как единственный параметр. Он возвращает true при наличии в массиве хотя бы одного нужного элемента и false — при отсутствии. Метод хорошо подходит для работы с разрешениями:

const users = [
 {
   id: 'fe34',
   permissions: ['read', 'write'],
 },
 {
   id: 'a198',
   permissions: [],
 },
 {
   id: '18aa',
   permissions: ['delete', 'read', 'write'],
 }
]

const hasDeletePermission = users.some(user =>
 user.permissions.includes('delete')
)

console.log(hasDeletePermission) // true

every

Идентичен предыдущему методу, но возвращает true в случае, если все элементы проходят проверку (а не минимум один).

const users = [
 {
   id: 'fe34',
   permissions: ['read', 'write'],
 },
 {
   id: 'a198',
   permissions: [],
 },
 {
   id: '18aa',
   permissions: ['delete', 'read', 'write'],
 }
]

const hasAllReadPermission = users.every(user =>
 user.permissions.includes('read')
)

console.log(hasAllReadPermission) // false

flat (стандарт ES2019)

Этот метод — новинка в мире JavaScript. flat() создаёт новый массив из всех подмассивов в нём. Он принимает один параметр — глубину «сглаживания» массива:

const numbers = [1, 2, [3, 4, [5, [6, 7]], [[[[8]]]]]]

const numbersflattenOnce = numbers.flat()
console.log(numbersflattenOnce) // [1, 2, 3, 4, Array[2], Array[1]]

const numbersflattenTwice = numbers.flat(2)
console.log(numbersflattenTwice) // [1, 2, 3, 4, 5, Array[2], Array[1]]

const numbersFlattenInfinity = numbers.flat(Infinity)
console.log(numbersFlattenInfinity) // [1, 2, 3, 4, 5, 6, 7, 8]

flatMap (стандарт ES2019)

Исходя из названия, несложно догадаться, что делает этот метод.

Сначала он вызывает mapping-функцию для каждого элемента в массиве, а потом «выравнивает» их в один массив. И всё!

const sentences = [
 'Это предложение',
 'Это уже другое предложение',
 "Не могу ничего найти",
]

const allWords = sentences.flatMap(sentence => sentence.split(' '))

console.log(allWords) // ["Это", "предложение", "Это", "уже", "другое", "предложение", "Не", "могу", "ничего", "найти"]

В примере выше нужно получить все слова из нескольких предложений. Вместо того, чтобы сначала использовать map() для разделения предложения на слова, а потом соединять их в массив, проще сразу использовать flatMap().

А ещё помощью reduce() можно подсчитать количество слов:

const wordsCount = allWords.reduce((count, word) => {
 count[word] = count[word] ? count[word] + 1 : 1
 return count
}, {})

console.log(wordsCount) // { "Не": 1, "Это": 2, "другое": 1, "могу": 1, "найти": 1, 
"ничего": 1, "предложение": 2, "уже": 1}

flatMap() часто используется в Реактивном Программировании, например как вот здесь.

join

Используйте этот метод в том случае, если вам нужно создать строку, базируясь на элементах массива. Этот метод позволяет создать строку, соединив все элементы массива через указанный разделитель.

Так, например, можно отобразить список всех участников:

const participants = ['john', 'mary', 'gary']
const participantsFormatted = participants.join(', ')
console.log(participantsFormatted) // john, mary, gary

А вот более практичный способ. Допустим, сначала нужно отфильтровать список участников перед соединением их имён в строку:

const potentialParticipants = [
 { id: 'k38i', name: 'john', age: 17 },
 { id: 'baf3', name: 'mary', age: 13 },
 { id: 'a111', name: 'gary', age: 24 },
 { id: 'fx34', name: 'emma', age: 34 },
]

const participantsFormatted = potentialParticipants
 .filter(user => user.age > 18)
 .map(user => user.name)
 .join(', ')

console.log(participantsFormatted) // gary, emma

from

Это статический метод, позволяющий создать новый массив из массиво-подобных и итерабельных объектов (строка). Метод полезен при работе с DOM.

const nodes = document.querySelectorAll('.todo-item') // Это экземпляр NodeList

const todoItems = Array.from(nodes) // Теперь можно использовать привычные map, filter и т.п. 

Вы заметили, что в примере выше вместо объекта массива используется Array? Именно поэтому метод from() называется статическим.

С помощью forEach() можно легко повесить на каждый элемент массива обработчик событий:

todoItems.forEach(item => {
 item.addEventListener('click', function() {
   alert(`Вы нажали на ${item.innerHTML}`)
 })
})

Модифицирующие методы

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

В случае, если вам всё же нужно сохранить оригинальный массив, можно использовать поверхностное или глубокое копирование массива:

const arr = [1, 2, 3, 4, 5]
const copy = [...arr] // или arr.slice()

sort

Да, sort() модифицирует оригинальный массив. По умолчанию, метод преобразует все элементы в строки и выполняет их сортировку в алфавитном порядке:

const names = ['john', 'mary', 'gary', 'anna']
names.sort()
console.log(names) // ['anna', 'gary', 'john', 'mary']

Поэтому будьте осторожны, придя, к примеру, с Python! При попытке выполнить sort для массива чисел можно получить совсем не тот результат, какой хотелось бы:

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort()
console.log(numbers) // [12, 17, 187, 23, 3, 90]

Как же в таком случае отсортировать массив? Метод sort() принимает всего один параметр — функцию сравнения. Эта функция принимает два параметра: первый элемент (например a) и второй элемент (b). Функция сравнения этих двух элементов должна возвращать число:

  • отрицательное, если a должно стоять перед b,
  • положительное, если b должно стоять перед a,
  • ноль, если значения равны и не требуют перестановки.

И теперь массив чисел можно сортировать так:

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort((a, b) => a - b)
console.log(numbers) // [3, 12, 17, 23, 90, 187]

Таким же образом можно сортировать даты (самые ранние):

const posts = [
 {
   title: 'Создание бота для Discord менее чем за 15 минут',
   date: new Date(2018, 11, 26),
 },
 {
   title: 'Как улучшить свой CSS',
   date: new Date(2018, 06, 17) },
 {
   title: 'Массивы в JavaScript',
   date: new Date()
 },
]

posts.sort((a, b) => a.date - b.date) // Вычитание двух дат вернёт разницу между ними в миллисекундах

console.log(posts)
// [ { title: 'Как улучшить свой CSS',
//     date: 2018-07-17T00:00:00.000Z },
//   { title: 'Создание бота для Discord менее чем за 15 минут',
//     date: 2018-12-26T00:00:00.000Z },
//   { title: 'Массивы в JavaScript',
//     date: 2019-03-16T10:31:00.208Z } ]

fill

Метод fill() изменяет или полностью заполняет массив с начальной по конечную позиции. Отличное применение — это заполнение нового массива одним статическим значением.

// В подобных случаях можно было бы вызвать созданную функцию, которая генерирует ID и имёна, но зачем париться 🙂
function fakeUser() {
 return {
   id: 'fe38',
   name: 'thomas',
 }
}

const posts = Array(3).fill(fakeUser())
console.log(posts) // [{ id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }]

reverse

Здесь всё предельно ясно:

const numbers = [1, 2, 3, 4, 5]
numbers.reverse()
console.log(numbers) // [5, 4, 3, 2, 1]

pop

Этот метод удаляет последний элемент массива и возвращает его.

const messages = ['Привет', 'Эй!', 'Ты как?', "Та вроде нормально"]
const lastMessage = messages.pop()
console.log(messages) // ['Привет', 'Эй!', 'Ты как?']
console.log(lastMessage) // Та вроде нормально

Методы, которые можно заменить

В конце этой статьи будет рассказано о методах, которые образованы из вышеописанных методов и могут быть легко заменены чем-нибудь другим. Необязательно отказываться от этих методов, просто помните, что есть альтернатива.

push

При работе с массивами этот метод используется довольно часто. Фактически он добавляет один элемент в конец массива. Метод push() также иногда используется для создания нового массива на основе уже существующего.

const todoItems = [1, 2, 3, 4, 5]
const itemsIncremented = []
for (let i = 0; i < todoItems.length; i++) {
 itemsIncremented.push(todoItems[i] + 1)
}

console.log(itemsIncremented) // [2, 3, 4, 5, 6]

const todos = ['Написание статьи', 'Вычитка']
todos.push('Публикация')
console.log(todos) // ['Написание статьи', 'Вычитка', 'Публикация']

Если вам нужно создать новый массив на основе уже существующего (как в itemsIncremented), то можно воспользоваться известными методами map(), filter(), reduce(). Например, с помощью map() создание нового массива выглядело бы так:

const itemsIncremented = todoItems.map(x => x + 1)

А если нужно будет добавить новый элемент в конец массива, кроме push() можно использовать spread-оператор:

const todos = ['Написание статьи', 'Вычитка'] 
console.log([...todos, 'Публикация'])

splice

Метод используется в тех случаях, когда нужно удалить элемент где-то в середине массива. Хотя тоже самое можно сделать и с помощью filter():

const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май']

// С помощью splice
months.splice(2, 1) // Удалить один элемент начиная со 2-й позиции
console.log(months) // ['Январь', 'Февраль', 'Апрель', 'Май']

// Забыв о splice()
const monthsFiltered = months.filter((month, i) => i !== 3)
console.log(monthsFiltered) // ['Январь', 'Февраль', 'Апрель', 'Май']

Всё бы хорошо, но как в таком случае удалить несколько элементов? Используя slice(), конечно:

const months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май']

// С помощью splice()
months.splice(1, 3) // Удалить 3 элемента, начиная с позиции 1
console.log(months) // ['Январь', 'Май']

// Забыв о splice()
const monthsFiltered = [...months.slice(0, 1), ...months.slice(4)]
console.log(monthsFiltered) // ['Январь','Май']

shift

Этот метод удаляет первый элемент массива и возвращает его. Подобного  можно добиться с помощью spread или rest операторов:

const numbers = [1, 2, 3, 4, 5]

// С помощью shift()
const firstNumber = numbers.shift()
console.log(firstNumber) // 1
console.log(numbers) // [2, 3, 4, 5]

// Забыв о shift()
const [firstNumber, ...numbersWithoutOne] = numbers
console.log(firstNumber) // 1
console.log(numbersWithoutOne) // [2, 3, 4, 5]

unshift

С помощью этого метода можно добавлять элементы в начало массива. Как и в предыдущем случае, unshift() можно заменить spread-оператором:

const numbers = [3, 4, 5]

// С помощью unshift
numbers.unshift(1, 2)
console.log(numbers) // [1, 2, 3, 4, 5]

// Забыв о unshift
const newNumbers = [1, 2, ...numbers]
console.log(newNumbers) // [1, 2, 3, 4, 5]

Важно помнить

  1. Каждый раз, когда нужно обработать массив, не обязательно использовать циклы или изобретать велосипед. Вероятнее всего, это уже сделали за вас. Поищите подходящий метод.
  2. В большинстве случаев задачу можно будет решить с помощью методов map(), filter(), reduce() или spread-оператора.
  3. Никогда не помешает умение применять методы slice(), some(), flatMap() и тому подобные. Используйте их, когда это будет целесообразно.
  4. Всегда помните, какие из методов создают новый массив, а какие модифицируют уже существующий. Иначе можно наломать дров.
  5. Метод slice() и spread-оператор делают поверхностную копию массива. Поэтому массивы и подмассивы будут ссылаться на один и тот же объект в памяти.
  6. «Старые» методы, изменяющие массив, имеют современные аналоги. Тщательно выбирайте используемые методы.

Перевод статьи «What you should know about JavaScript arrays»

Также рекомендуем:

Рубрика: Переводы
Темы: JavaScriptЛучшая практика