В прошлом уроке мы бегло познакомились с массивами — ровно настолько, чтобы был повод использовать циклы. Сегодня разберём их полноценно, вместе с объектами — и увидим, почему именно связка "массив объектов" является той формой, в которой к вам придут данные практически из любого API, включая тот самый Blog API из курса python-sql-fastapi, к которому мы обязательно вернёмся в финале этого модуля.
Взгляните на форму, которую примет ответ сервера, когда мы (уже совсем скоро) получим список постов блога через fetch:
const posts = [
{ id: 1, title: 'Первый пост', content: 'Текст...', author_id: 3 },
{ id: 2, title: 'Второй пост', content: 'Другой текст...', author_id: 1 },
];Это ровно то, что мы сегодня разберём: массив, каждый элемент которого — объект. Вся сегодняшняя тема — это подготовка именно к такому виду данных.
Объект в JavaScript — прямой аналог словаря (dict) в Python: набор пар "ключ-значение", где ключ — обычно строка (хотя кавычки часто опускают, если ключ — валидное имя переменной).
const movie = {
title: 'Матрица',
year: 1999,
rating: 8.7,
isAvailable: true,
};
console.log(movie.title); // Матрица — доступ через точку
console.log(movie['year']); // 1999 — доступ через квадратные скобки, как в Python movie['year']console.log(movie.title); // работает, потому что 'title' — валидное имя свойства
const key = 'title';
console.log(movie[key]); // 'Матрица' — квадратные скобки нужны, когда ключ хранится в переменной
console.log(movie['release year']); // квадратные скобки обязательны, если ключ содержит пробел или спецсимволыПравило: используйте точку, если имя свойства известно заранее и записано напрямую в коде. Используйте квадратные скобки, если имя свойства приходит из переменной (динамически) или содержит символы, недопустимые в обычном имени (пробелы, дефисы).
movie.rating = 9.0; // изменение существующего свойства
movie.director = 'Вачовски'; // добавление нового свойства — объект не обязан быть "жёстко зафиксирован" по составу полей
console.log(movie);
// { title: 'Матрица', year: 1999, rating: 9.0, isAvailable: true, director: 'Вачовски' }const post = {
title: 'Первый пост',
author: {
name: 'Мария',
id: 3,
},
getSummary: function () {
return `${this.title} (автор: ${this.author.name})`;
},
};
console.log(post.author.name); // Мария — доступ к вложенному объекту через цепочку точек
console.log(post.getSummary()); // Первый пост (автор: Мария)getSummary здесь — метод объекта, функция, "принадлежащая" этому конкретному объекту. Ключевое слово this внутри метода ссылается на сам объект, у которого этот метод был вызван (post, в данном случае) — это прямая параллель c self в методах Python-классов, хотя технически this в JS устроен тоньше и с большим числом нюансов, которые мы подробно разберём в следующем уроке про классы.
const movie = { title: 'Матрица', year: 1999, rating: 8.7 };
console.log(Object.keys(movie)); // ['title', 'year', 'rating']
console.log(Object.values(movie)); // ['Матрица', 1999, 8.7]
console.log(Object.entries(movie)); // [['title', 'Матрица'], ['year', 1999], ['rating', 8.7]]Эти три метода превращают объект в массив — ключей, значений или пар — что особенно полезно, когда нужно перебрать содержимое объекта в цикле:
for (const [key, value] of Object.entries(movie)) {
console.log(`${key}: ${value}`);
}
// title: Матрица
// year: 1999
// rating: 8.7const movies = ['Матрица', 'Начало', 'Интерстеллар'];
movies.push('Дюна'); // добавляет элемент в конец
console.log(movies); // ['Матрица', 'Начало', 'Интерстеллар', 'Дюна']
movies.pop(); // удаляет и возвращает последний элемент
console.log(movies); // ['Матрица', 'Начало', 'Интерстеллар']
console.log(movies.includes('Начало')); // true — есть ли значение в массиве
console.log(movies.indexOf('Начало')); // 1 — индекс значения (или -1, если не найдено)const movies = [
{ title: 'Матрица', year: 1999, rating: 8.7 },
{ title: 'Начало', year: 2010, rating: 8.8 },
{ title: 'Дюна', year: 2021, rating: 8.0 },
{ title: 'Аватар', year: 2009, rating: 7.8 },
];Именно такой формой данных мы теперь будем пользоваться почти во всех примерах — она максимально приближена к тому, что реально приходит с сервера.
const movies = [
{ title: 'Матрица', year: 1999 },
{ title: 'Начало', year: 2010 },
];
movies.forEach((movie) => {
console.log(`${movie.title} (${movie.year})`);
});
// Матрица (1999)
// Начало (2010)forEach принимает функцию как аргумент (это называется callback-функция — "функция обратного вызова", которую сам метод вызовет для каждого элемента) и выполняет её для каждого элемента массива по очереди. По смыслу это очень похоже на for...of из прошлого урока — но записано как вызов метода, а не как отдельная конструкция языка.
movies.forEach((movie, index, array) => {
console.log(`${index + 1} из ${array.length}: ${movie.title}`);
});
// 1 из 2: Матрица
// 2 из 2: НачалоИндекс и сам массив нужны редко, но полезно знать, что они доступны — не приходится, как с классическим for, заводить отдельный счётчик вручную, если индекс всё же понадобился.
const result = movies.forEach((movie) => movie.title);
console.log(result); // undefinedforEach создан исключительно для выполнения действия (обычно побочного эффекта — вывода в консоль, изменения внешней переменной) для каждого элемента. Он не создаёт новый массив и не предназначен для трансформации данных — для этого существует map.
const movies = [
{ title: 'Матрица', rating: 8.7 },
{ title: 'Начало', rating: 8.8 },
{ title: 'Дюна', rating: 8.0 },
];
const titles = movies.map((movie) => movie.title);
console.log(titles); // ['Матрица', 'Начало', 'Дюна']map вызывает переданную функцию для каждого элемента и собирает возвращённые значения в новый массив той же длины, что и исходный. Исходный массив movies при этом остаётся неизменным.
// Более сложное преобразование — из массива объектов в массив строк
const summaries = movies.map((movie) => `${movie.title} — ${movie.rating}/10`);
console.log(summaries);
// ['Матрица — 8.7/10', 'Начало — 8.8/10', 'Дюна — 8/10']// map может превращать объекты в новые объекты — например, добавляя вычисляемое поле
const moviesWithLabel = movies.map((movie) => {
return {
...movie, // spread-оператор — копируем все существующие свойства объекта (разберём чуть ниже)
isTopRated: movie.rating >= 8.5,
};
});
console.log(moviesWithLabel);
// [
// { title: 'Матрица', rating: 8.7, isTopRated: true },
// { title: 'Начало', rating: 8.8, isTopRated: true },
// { title: 'Дюна', rating: 8.0, isTopRated: false },
// ]Прямая параллель с Python: map в JS — практически то же самое, что list comprehension ([f(x) for x in movies]) или встроенный map() в Python, просто с методным синтаксисом вместо отдельной функции/выражения.
const movies = [
{ title: 'Матрица', rating: 8.7 },
{ title: 'Начало', rating: 8.8 },
{ title: 'Дюна', rating: 8.0 },
{ title: 'Форсаж 9', rating: 4.5 },
];
const topRated = movies.filter((movie) => movie.rating >= 8);
console.log(topRated);
// [
// { title: 'Матрица', rating: 8.7 },
// { title: 'Начало', rating: 8.8 },
// { title: 'Дюна', rating: 8.0 },
// ]filter вызывает переданную функцию для каждого элемента; если функция возвращает true — элемент попадает в новый массив, если false — не попадает. Функция-предикат (условие) должна возвращать именно булево значение (или значение, которое приводится к true/false).
Прямая параллель с Python: это аналог list comprehension с условием ([m for m in movies if m['rating'] >= 8]) или встроенного filter().
const movies = [
{ title: 'Матрица', rating: 8.7 },
{ title: 'Начало', rating: 8.8 },
{ title: 'Дюна', rating: 8.0 },
];
const found = movies.find((movie) => movie.title === 'Начало');
console.log(found); // { title: 'Начало', rating: 8.8 }
const notFound = movies.find((movie) => movie.title === 'Титаник');
console.log(notFound); // undefined — если ничего не найденоКлючевое отличие от filter: find возвращает один объект (первый подходящий) или undefined, если ничего не найдено. filter всегда возвращает массив — даже если подошёл только один элемент (массив из одного элемента) или ни одного (пустой массив).
| Метод | Возвращает | Когда использовать |
|---|---|---|
forEach |
undefined (ничего) |
Выполнить действие для каждого элемента (вывод, побочный эффект) |
map |
Новый массив той же длины | Преобразовать каждый элемент в новое значение |
filter |
Новый массив (возможно, короче или пустой) | Оставить только элементы, подходящие условию |
find |
Один элемент или undefined |
Найти первый элемент, подходящий условию |
Методы массивов можно вызывать цепочкой, друг за другом — каждый следующий метод работает уже с результатом предыдущего:
const movies = [
{ title: 'Матрица', year: 1999, rating: 8.7 },
{ title: 'Начало', year: 2010, rating: 8.8 },
{ title: 'Дюна', year: 2021, rating: 8.0 },
{ title: 'Форсаж 9', year: 2021, rating: 4.5 },
];
const topRatedTitles = movies
.filter((movie) => movie.rating >= 8)
.map((movie) => movie.title);
console.log(topRatedTitles); // ['Матрица', 'Начало', 'Дюна']Сначала filter оставляет только фильмы с высоким рейтингом, затем map превращает оставшиеся объекты в массив одних лишь названий. Такая цепочка читается почти как естественный язык: "отфильтруй, затем преобразуй" — и является одним из самых частых и практичных паттернов работы с данными в реальном JS-коде.
const movie = { title: 'Матрица', year: 1999, rating: 8.7 };
const { title, year } = movie;
console.log(title); // Матрица
console.log(year); // 1999Вместо const title = movie.title; const year = movie.year; — одна строка, извлекающая сразу несколько свойств по их именам. Имена переменных слева должны совпадать с именами свойств объекта.
const movie = { title: 'Матрица', year: 1999 };
const { title: movieTitle, year: releaseYear } = movie;
console.log(movieTitle); // Матрица
console.log(releaseYear); // 1999const movie = { title: 'Матрица' };
const { title, rating = 0 } = movie; // rating отсутствует в объекте — используется значение по умолчанию
console.log(rating); // 0const coordinates = [55.75, 37.61];
const [latitude, longitude] = coordinates;
console.log(latitude); // 55.75
console.log(longitude); // 37.61В отличие от объектов, при деструктуризации массива имена переменных слева могут быть любыми — порядок важен, а не совпадение имён, потому что у элементов массива нет собственных "имён", только порядковая позиция.
Это едва ли не самое частое практическое применение — принимать сразу распакованный объект как параметр функции:
const printMovie = ({ title, year }) => {
console.log(`${title} (${year})`);
};
printMovie({ title: 'Матрица', year: 1999, rating: 8.7 });
// Матрица (1999) — обратите внимание, что rating просто проигнорирован, деструктуризация берёт только нужноеМы уже видели ... в примере с map выше — пришло время разобрать его отдельно.
const movie = { title: 'Матрица', year: 1999, rating: 8.7 };
const updatedMovie = { ...movie, rating: 9.0 };
console.log(updatedMovie); // { title: 'Матрица', year: 1999, rating: 9.0 }
console.log(movie); // { title: 'Матрица', year: 1999, rating: 8.7 } — исходный объект не изменился{ ...movie, rating: 9.0 } создаёт новый объект: сначала копирует все свойства movie, а затем перезаписывает rating новым значением. Порядок важен — если бы rating: 9.0 стоял перед ...movie, исходное значение rating из movie перезаписало бы новое.
const classics = ['Матрица', 'Терминатор'];
const newReleases = ['Дюна', 'Оппенгеймер'];
const allMovies = [...classics, ...newReleases];
console.log(allMovies); // ['Матрица', 'Терминатор', 'Дюна', 'Оппенгеймер']Почему это важно, а не просто "удобный синтаксис": мы уже знаем, что const не защищает объект/массив от изменения содержимого напрямую (Урок 16). Spread-оператор — это практический инструмент, позволяющий создавать новую копию данных вместо изменения существующей — важный принцип, к которому вы ещё вернётесь в теме про иммутабельность данных при работе с React или похожими инструментами в будущем (за пределами этого курса), но полезный уже сейчас для написания предсказуемого кода.
Соберём воедино всё из этого урока на одном связном примере.
const movies = [
{ title: 'Матрица', year: 1999, rating: 8.7, genre: 'Фантастика' },
{ title: 'Начало', year: 2010, rating: 8.8, genre: 'Фантастика' },
{ title: 'Дюна', year: 2021, rating: 8.0, genre: 'Фантастика' },
{ title: 'Форсаж 9', year: 2021, rating: 4.5, genre: 'Боевик' },
{ title: 'Крестный отец', year: 1972, rating: 9.2, genre: 'Драма' },
];
// 1. Вывести все названия через forEach
movies.forEach((movie) => console.log(movie.title));
// 2. Получить список фильмов только жанра "Фантастика" (filter)
const sciFiMovies = movies.filter((movie) => movie.genre === 'Фантастика');
console.log(sciFiMovies);
// 3. Получить массив строк "Название (Год)" (map)
const formattedList = movies.map((movie) => `${movie.title} (${movie.year})`);
console.log(formattedList);
// 4. Найти конкретный фильм по названию (find)
const dune = movies.find((movie) => movie.title === 'Дюна');
console.log(dune);
// 5. Комбинация: топовые фильмы (rating >= 8.5), только названия, через цепочку
const bestTitles = movies
.filter((movie) => movie.rating >= 8.5)
.map((movie) => movie.title);
console.log(bestTitles); // ['Начало', 'Крестный отец']
// 6. Деструктуризация при выводе внутри forEach
movies.forEach(({ title, rating }) => {
console.log(`${title}: ${rating}/10`);
});Обратите внимание на последний пример: деструктуризация прямо в параметре callback-функции (({ title, rating }) => ...) — комбинация двух тем сегодняшнего урока, которая на практике встречается очень часто.
// forEach не создаёт новый массив — переменная получит undefined
const titles = movies.forEach((movie) => movie.title);
console.log(titles); // undefined// Если нужен новый массив — используйте map, а не forEach
const titles = movies.map((movie) => movie.title);
console.log(titles); // ['Матрица', 'Начало', ...]Это одна из самых частых ошибок новичков: forEach и map выглядят синтаксически почти одинаково, но решают принципиально разные задачи — выполнение действия против трансформации данных в новый массив.
const result = movies.filter((movie) => movie.title === 'Дюна');
console.log(result); // [{ title: 'Дюна', ... }] — это МАССИВ из одного элемента, а не сам объект!
console.log(result.title); // undefined — у массива нет свойства title
console.log(result[0].title); // 'Дюна' — так придётся обращаться, если использовать filter вместо findЕсли нужен единственный объект — используйте find. Если действительно нужен список из потенциально нескольких (или нуля) элементов — filter.
// Плохо: напрямую изменяем объект исходного массива внутри map
const updated = movies.map((movie) => {
movie.rating = 10; // мутируем исходный объект — тот же объект остаётся и в исходном massive!
return movie;
});
console.log(movies[0].rating); // 10 — исходный массив тоже изменился, хотя мы этого не планировали!// Хорошо: создаём новый объект через spread, не трогая исходный
const updated = movies.map((movie) => ({ ...movie, rating: 10 }));
console.log(movies[0].rating); // прежнее значение — исходный массив остался нетронутымМассивы и объекты в JavaScript передаются "по ссылке" — если внутри map напрямую изменить свойства объекта-параметра, это изменит тот же самый объект, на который ссылается и исходный массив, потому что оба указывают на одну и ту же область памяти. Spread-оператор создаёт новый, независимый объект — и это первый шаг к тому, чтобы избегать подобных неожиданных побочных эффектов.
console.log({ title: 'Матрица' } === { title: 'Матрица' }); // false!=== для объектов и массивов сравнивает не содержимое, а ссылку — совпадают ли переменные, указывая буквально на один и тот же объект в памяти. Два визуально одинаковых, но отдельно созданных объекта никогда не будут равны через ===. Сравнение содержимого объектов — отдельная задача, которую мы пока не разбираем подробно (обычно решается либо сравнением конкретных полей вручную, либо специальными библиотечными функциями за пределами базового языка).
Объекты — структуры "ключ-значение", доступ к полям через точку (если имя известно заранее) или квадратные скобки (если имя приходит из переменной или содержит спецсимволы). Массив объектов — основная форма данных, с которой вы будете постоянно работать на практике, особенно получая ответы от серверных API.
Четыре метода массива решают разные задачи: forEach выполняет действие для каждого элемента без возврата результата; map преобразует каждый элемент в новое значение, возвращая новый массив той же длины; filter оставляет только подходящие условию элементы, возвращая новый (возможно, более короткий или пустой) массив; find возвращает первый подходящий элемент (или undefined), а не массив. Методы можно объединять цепочкой (filter().map()), что является одним из самых частых паттернов практического JS-кода.
Деструктуризация — компактный синтаксис извлечения значений из объектов и массивов в отдельные переменные, включая переименование, значения по умолчанию и прямое использование в параметрах функций. Spread-оператор (...) создаёт независимую копию объекта или массива, что помогает избегать неожиданного изменения исходных данных при работе с map и подобными методами.
В следующем уроке — классы в JavaScript: свойства, методы, конструктор, и то, как это соотносится с классами Python, которые вы уже хорошо знаете по ООП-курсу.
- В чём разница между обращением к свойству объекта через точку и через квадратные скобки? Когда обязательно нужны именно скобки?
- Чем
forEachпринципиально отличается отmap, если оба метода выполняют переданную функцию для каждого элемента массива? - Чем
filterотличается отfindс точки зрения того, что каждый из них возвращает? - Что делает деструктуризация объекта и чем она удобнее последовательных обращений через точку к каждому свойству по отдельности?
- Почему у деструктуризации объекта имена переменных слева должны совпадать с именами свойств, а у деструктуризации массива — нет?
- Что делает spread-оператор (
...) при создании нового объекта на основе существующего, и почему это важно при использованииmap? - Почему сравнение двух визуально одинаковых объектов через
===возвращаетfalse? - В цепочке
movies.filter(...).map(...)— что именно получает методmapв качестве входных данных: исходный массивmoviesили что-то другое?
Дан массив:
const movies = [
{ title: 'Матрица', year: 1999, rating: 8.7, genre: 'Фантастика' },
{ title: 'Зелёная миля', year: 1999, rating: 9.1, genre: 'Драма' },
{ title: 'Интерстеллар', year: 2014, rating: 8.6, genre: 'Фантастика' },
{ title: 'Джокер', year: 2019, rating: 8.4, genre: 'Драма' },
];- Используя
map, получите массив строк вида"Матрица (1999)"для всех фильмов. - Используя
filter, получите массив фильмов только жанра'Драма'. - Используя
find, найдите фильм сtitle === 'Джокер'и выведите его рейтинг. - Используя
forEachи деструктуризацию в параметре callback-функции, выведите строки вида"Матрица — 8.7"для каждого фильма.
Имея массив фильмов из Задания 1, создайте новый массив, в котором у каждого фильма добавлено новое поле isClassic: true, если year < 2000, иначе isClassic: false. Исходный массив movies при этом не должен измениться. Используйте map и spread-оператор.
Данные ниже намеренно оформлены в форме, максимально приближенной к тому, что вернёт наш Blog API (python-sql-fastapi) при запросе комментариев к посту:
const comments = [
{ id: 1, content: 'Отличный пост!', author_id: 2, post_id: 5 },
{ id: 2, content: 'Не согласен с автором', author_id: 4, post_id: 5 },
{ id: 3, content: 'Спасибо, полезно', author_id: 2, post_id: 5 },
{ id: 4, content: 'Есть неточность в третьем абзаце', author_id: 7, post_id: 5 },
];- Используя
filter, найдите все комментарии от автора сauthor_id === 2. - Используя
map, получите массив строк вида"Комментарий #1: Отличный пост!"(используйтеidиcontent). - Используя
find, найдите комментарий сid === 3и выведите его содержимое через деструктуризацию (const { content } = ...). - Скомбинируйте
filterиmapв одной цепочке: получите массив содержимого (content) только тех комментариев, которые длиннее 15 символов.