Skip to content

Latest commit

 

History

History
560 lines (397 loc) · 34.1 KB

File metadata and controls

560 lines (397 loc) · 34.1 KB

Урок 18. Массивы и их методы (map, filter, forEach, find), объекты, деструктуризация

Данные в реальном фронтенде почти всегда выглядят так

В прошлом уроке мы бегло познакомились с массивами — ровно настолько, чтобы был повод использовать циклы. Сегодня разберём их полноценно, вместе с объектами — и увидим, почему именно связка "массив объектов" является той формой, в которой к вам придут данные практически из любого 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']

Точка vs квадратные скобки

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 устроен тоньше и с большим числом нюансов, которые мы подробно разберём в следующем уроке про классы.

Object.keys, Object.values, Object.entries

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.7

Массивы: полноценный набор инструментов

Создание и базовые операции

const 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 },
];

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


forEach — выполнить действие для каждого элемента

const movies = [
  { title: 'Матрица', year: 1999 },
  { title: 'Начало', year: 2010 },
];

movies.forEach((movie) => {
  console.log(`${movie.title} (${movie.year})`);
});
// Матрица (1999)
// Начало (2010)

forEach принимает функцию как аргумент (это называется callback-функция — "функция обратного вызова", которую сам метод вызовет для каждого элемента) и выполняет её для каждого элемента массива по очереди. По смыслу это очень похоже на for...of из прошлого урока — но записано как вызов метода, а не как отдельная конструкция языка.

Callback получает три аргумента: элемент, индекс, весь массив

movies.forEach((movie, index, array) => {
  console.log(`${index + 1} из ${array.length}: ${movie.title}`);
});
// 1 из 2: Матрица
// 2 из 2: Начало

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

Важное ограничение: forEach ничего не возвращает

const result = movies.forEach((movie) => movie.title);
console.log(result); // undefined

forEach создан исключительно для выполнения действия (обычно побочного эффекта — вывода в консоль, изменения внешней переменной) для каждого элемента. Он не создаёт новый массив и не предназначен для трансформации данных — для этого существует map.


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, просто с методным синтаксисом вместо отдельной функции/выражения.


filter — оставить только подходящие элементы

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().


find — найти первый подходящий элемент

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 Найти первый элемент, подходящий условию

Комбинирование методов (chaining)

Методы массивов можно вызывать цепочкой, друг за другом — каждый следующий метод работает уже с результатом предыдущего:

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); // 1999

Значения по умолчанию при деструктуризации

const movie = { title: 'Матрица' };

const { title, rating = 0 } = movie; // rating отсутствует в объекте — используется значение по умолчанию
console.log(rating); // 0

Деструктуризация массива

const 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 просто проигнорирован, деструктуризация берёт только нужное

Spread-оператор (...) — быстрое копирование и объединение

Мы уже видели ... в примере с 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 там, где нужен map

// 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 выглядят синтаксически почти одинаково, но решают принципиально разные задачи — выполнение действия против трансформации данных в новый массив.

filter возвращает массив, даже если подошёл только один элемент — путают с find

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 вместо создания копии

// Плохо: напрямую изменяем объект исходного массива внутри 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, которые вы уже хорошо знаете по ООП-курсу.


Вопросы для проверки

  1. В чём разница между обращением к свойству объекта через точку и через квадратные скобки? Когда обязательно нужны именно скобки?
  2. Чем forEach принципиально отличается от map, если оба метода выполняют переданную функцию для каждого элемента массива?
  3. Чем filter отличается от find с точки зрения того, что каждый из них возвращает?
  4. Что делает деструктуризация объекта и чем она удобнее последовательных обращений через точку к каждому свойству по отдельности?
  5. Почему у деструктуризации объекта имена переменных слева должны совпадать с именами свойств, а у деструктуризации массива — нет?
  6. Что делает spread-оператор (...) при создании нового объекта на основе существующего, и почему это важно при использовании map?
  7. Почему сравнение двух визуально одинаковых объектов через === возвращает false?
  8. В цепочке movies.filter(...).map(...) — что именно получает метод map в качестве входных данных: исходный массив movies или что-то другое?

Практические задания

Задание 1. Каталог фильмов — базовые операции с массивом объектов

Дан массив:

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: 'Драма' },
];
  1. Используя map, получите массив строк вида "Матрица (1999)" для всех фильмов.
  2. Используя filter, получите массив фильмов только жанра 'Драма'.
  3. Используя find, найдите фильм с title === 'Джокер' и выведите его рейтинг.
  4. Используя forEach и деструктуризацию в параметре callback-функции, выведите строки вида "Матрица — 8.7" для каждого фильма.

Задание 2. Обновление данных без мутации исходного массива

Имея массив фильмов из Задания 1, создайте новый массив, в котором у каждого фильма добавлено новое поле isClassic: true, если year < 2000, иначе isClassic: false. Исходный массив movies при этом не должен измениться. Используйте map и spread-оператор.


Задание 3. Комментарии к посту — объединяющая практика (мост к Blog API)

Данные ниже намеренно оформлены в форме, максимально приближенной к тому, что вернёт наш 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 },
];
  1. Используя filter, найдите все комментарии от автора с author_id === 2.
  2. Используя map, получите массив строк вида "Комментарий #1: Отличный пост!" (используйте id и content).
  3. Используя find, найдите комментарий с id === 3 и выведите его содержимое через деструктуризацию (const { content } = ...).
  4. Скомбинируйте filter и map в одной цепочке: получите массив содержимого (content) только тех комментариев, которые длиннее 15 символов.

Предыдущий урок | Следующий урок