В прошлом уроке мы работали с объектами, которые создавались вручную, каждый со своим набором фигурных скобок:
const movie1 = { title: 'Матрица', year: 1999, rating: 8.7 };
const movie2 = { title: 'Начало', year: 2010, rating: 8.8 };Это прекрасно работает для пары объектов, но что если таких фильмов сотни, и для каждого нужно ещё и одинаковое поведение — метод, который считает возрастной рейтинг, метод, который форматирует строку для вывода? Копировать одну и ту же логику в каждый объект вручную — плохая идея. Именно эту проблему решают классы: шаблон, по которому создаются однотипные объекты с одинаковой структурой и одинаковым поведением.
Вы уже прекрасно знаете классы — из ООП-курса на Python. Хорошая новость: сама концепция классов в JavaScript устроена почти так же — конструктор, свойства, методы, this/self. Этот урок будет во многом опираться на прямые параллели с тем, что вы уже знаете, обращая особое внимание на синтаксические и смысловые различия там, где они есть.
class Movie {
constructor(title, year, rating) {
this.title = title;
this.year = year;
this.rating = rating;
}
}
const movie1 = new Movie('Матрица', 1999, 8.7);
const movie2 = new Movie('Начало', 2010, 8.8);
console.log(movie1.title); // Матрица
console.log(movie2.year); // 2010Сопоставим с тем же классом на Python, который выглядел бы так:
class Movie:
def __init__(self, title, year, rating):
self.title = title
self.year = year
self.rating = rating
movie1 = Movie('Матрица', 1999, 8.7)
movie2 = Movie('Начало', 2010, 8.8)Прямые соответствия:
| Python | JavaScript | Назначение |
|---|---|---|
class Movie: |
class Movie {} |
Объявление класса |
def __init__(self, ...): |
constructor(...) {} |
Специальный метод, вызываемый при создании объекта |
self |
this |
Ссылка на текущий, конкретный экземпляр класса |
Movie('Матрица', ...) |
new Movie('Матрица', ...) |
Создание экземпляра — но в JS обязательно ключевое слово new |
Самое важное синтаксическое отличие: в JavaScript при создании экземпляра класса обязательно ключевое слово new перед вызовом конструктора. Забыть его — частая ошибка новичков, разберём в подводных камнях.
this внутри класса ссылается на тот конкретный экземпляр, для которого сейчас выполняется код — совершенно аналогично self в Python. Разница лишь в том, что self в Python явно указывается первым параметром метода, а this в JS доступен неявно, без объявления в списке параметров.
class Movie {
constructor(title, year, rating) {
this.title = title;
this.year = year;
this.rating = rating;
}
getSummary() {
return `${this.title} (${this.year}) — ${this.rating}/10`;
}
isTopRated() {
return this.rating >= 8.5;
}
}
const movie = new Movie('Матрица', 1999, 8.7);
console.log(movie.getSummary()); // Матрица (1999) — 8.7/10
console.log(movie.isTopRated()); // trueОбратите внимание: у методов класса нет запятых между ними и нет ключевого слова function перед именем метода — это специальный, более компактный синтаксис, доступный именно внутри тела class. Внутри каждого метода this по-прежнему ссылается на конкретный экземпляр, на котором метод был вызван (movie, в данном случае).
Точно так же, как для обычных функций (Урок 17), параметры конструктора могут иметь значения по умолчанию:
class Movie {
constructor(title, year, rating = 0) {
this.title = title;
this.year = year;
this.rating = rating;
}
}
const movie = new Movie('Неизвестный фильм', 2024);
console.log(movie.rating); // 0 — использовано значение по умолчаниюИногда нужно, чтобы обращение к свойству объекта выглядело как обычное чтение поля (movie.ageLabel, без скобок вызова функции), но на самом деле вычислялось на лету. Для этого существуют get и set:
class Movie {
constructor(title, year, rating) {
this.title = title;
this.year = year;
this.rating = rating;
}
get ageLabel() {
const currentYear = new Date().getFullYear();
const age = currentYear - this.year;
return age === 0 ? 'Новинка' : `${age} лет назад`;
}
}
const movie = new Movie('Матрица', 1999, 8.7);
console.log(movie.ageLabel); // например, "26 лет назад" — обратите внимание: без скобок ()!Это прямой аналог @property в Python-классах, которые вы уже видели в ООП-курсе — та же идея "вычисляемого свойства, которое выглядит как обычное поле", просто другой синтаксис (get в JS вместо декоратора @property в Python).
set работает симметрично — позволяет перехватить присваивание значения свойству и выполнить какую-то логику вместо простого сохранения:
class Movie {
constructor(title, rating) {
this.title = title;
this._rating = rating; // подчёркивание — соглашение, что поле "внутреннее", как и в Python
}
get rating() {
return this._rating;
}
set rating(value) {
if (value < 0 || value > 10) {
throw new Error('Рейтинг должен быть от 0 до 10');
}
this._rating = value;
}
}
const movie = new Movie('Матрица', 8.7);
movie.rating = 9.5; // вызывает set — валидация происходит автоматически
console.log(movie.rating); // 9.5 — обращение к get
movie.rating = 15; // Error: Рейтинг должен быть от 0 до 10class Movie {
static genreList = ['Фантастика', 'Драма', 'Комедия', 'Боевик'];
constructor(title, genre) {
this.title = title;
this.genre = genre;
}
static isValidGenre(genre) {
return Movie.genreList.includes(genre);
}
}
console.log(Movie.genreList); // ['Фантастика', 'Драма', 'Комедия', 'Боевик']
console.log(Movie.isValidGenre('Драма')); // true
console.log(Movie.isValidGenre('Ужасы')); // false
const movie = new Movie('Матрица', 'Фантастика');
console.log(movie.genreList); // undefined — статическое свойство недоступно через экземпляр!static делает свойство или метод принадлежащим самому классу, а не отдельным экземплярам — прямой аналог @staticmethod/@classmethod и атрибутов класса (ClassVar из dataclasses, если вспомнить пример из ООП-курса) в Python. Обращение происходит через имя класса (Movie.genreList), а не через конкретный экземпляр (movie.genreList вернёт undefined, потому что статическое свойство не является частью каждого отдельного объекта).
Уместно использовать static, когда данные или логика относятся ко всем экземплярам класса сразу, а не к какому-то одному конкретному фильму — например, список допустимых жанров, счётчик созданных экземпляров, вспомогательная функция валидации.
Современный JavaScript поддерживает по-настоящему приватные поля класса — в отличие от соглашения об именовании через нижнее подчёркивание в Python (_protected), которое остаётся лишь договорённостью между разработчиками, а не техническим ограничением:
class Movie {
#internalId; // приватное поле — недоступно снаружи класса вообще
constructor(title, id) {
this.title = title;
this.#internalId = id;
}
getId() {
return this.#internalId; // доступ изнутри класса — работает
}
}
const movie = new Movie('Матрица', 42);
console.log(movie.getId()); // 42 — доступ через публичный метод работает
console.log(movie.#internalId); // SyntaxError — прямой доступ снаружи класса запрещён на уровне синтаксисаВажное отличие от Python: нижнее подчёркивание в Python (self._rating) — это лишь соглашение, "джентльменский договор" между разработчиками, что поле не предназначено для внешнего использования, но технически ничто не мешает обратиться к нему напрямую снаружи. Символ # в JavaScript — это по-настоящему приватное поле на уровне синтаксиса языка: обратиться к нему извне класса физически невозможно, попытка вызовет ошибку.
Вы уже подробно разбирали наследование в Python-курсе — здесь разберём его аналог в JS кратко, поскольку смысловая часть та же самая, меняется в основном синтаксис.
class Content {
constructor(title, year) {
this.title = title;
this.year = year;
}
getSummary() {
return `${this.title} (${this.year})`;
}
}
class Movie extends Content {
constructor(title, year, duration) {
super(title, year); // вызов конструктора родительского класса Content
this.duration = duration;
}
getSummary() {
return `${super.getSummary()}, длительность: ${this.duration} мин.`;
}
}
const movie = new Movie('Матрица', 1999, 136);
console.log(movie.getSummary()); // Матрица (1999), длительность: 136 мин.extends объявляет, что Movie наследует от Content — прямой аналог class Movie(Content): в Python. super(...) внутри конструктора вызывает конструктор родительского класса — аналог super().__init__(...) в Python. super.getSummary() внутри переопределённого метода вызывает родительскую версию метода перед добавлением собственной логики — аналог super().get_summary().
Мы обещали вернуться к этой теме ещё в Уроке 17 — сейчас самое время. Рассмотрим ситуацию, в которой метод класса передаётся как callback-функция, например, в forEach:
class MovieCatalog {
constructor(movies) {
this.movies = movies;
this.catalogName = 'Мой каталог';
}
// Обычный метод — здесь this работает предсказуемо, пока вызывается как movieCatalog.printAll()
printAll() {
this.movies.forEach(function (movie) {
// ⚠️ Внутри обычной function this — уже НЕ экземпляр MovieCatalog!
console.log(this.catalogName, movie.title); // TypeError или undefined
});
}
}Проблема в том, что у обычной function, переданной как callback внутрь forEach, — собственное, отдельное значение this, никак не связанное с тем this, что было снаружи (this конкретного экземпляра MovieCatalog). Это одна из самых частых причин путаницы у новичков в JS.
Решение — стрелочная функция:
class MovieCatalog {
constructor(movies) {
this.movies = movies;
this.catalogName = 'Мой каталог';
}
printAll() {
this.movies.forEach((movie) => {
// ✅ Стрелочная функция НЕ создаёт собственный this —
// она "наследует" this из окружающего кода (из метода printAll)
console.log(this.catalogName, movie.title);
});
}
}
const catalog = new MovieCatalog([{ title: 'Матрица' }, { title: 'Начало' }]);
catalog.printAll();
// Мой каталог Матрица
// Мой каталог НачалоЭто и есть та самая "более глубокая техническая причина" использовать стрелочные функции, которую мы анонсировали в Уроке 17: стрелочные функции не создают собственного this — они прозрачно используют this того места в коде, где стрелочная функция была написана. Внутри методов класса, при передаче функции как callback (в forEach, map, filter и подобные), это делает стрелочные функции значительно безопаснее и предсказуемее обычных.
Практическое правило: внутри методов класса, если требуется передать функцию как callback (в forEach/map/filter/обработчик события, который мы разберём в уроке про DOM), всегда используйте стрелочную функцию, а не function, если внутри callback нужно обратиться к this самого экземпляра класса.
Соберём воедино классы этого урока и методы массивов из прошлого урока в единую, связную систему — прямое продолжение практики Урока 18.
class Movie {
constructor(title, year, rating, genre) {
this.title = title;
this.year = year;
this.rating = rating;
this.genre = genre;
}
get summary() {
return `${this.title} (${this.year}) — ${this.rating}/10`;
}
isTopRated() {
return this.rating >= 8.5;
}
}
class MovieCatalog {
constructor() {
this.movies = [];
}
addMovie(movie) {
this.movies.push(movie);
}
filterByGenre(genre) {
return this.movies.filter((movie) => movie.genre === genre);
}
getTopRated() {
return this.movies.filter((movie) => movie.isTopRated());
}
printAll() {
this.movies.forEach((movie) => {
console.log(movie.summary);
});
}
}
const catalog = new MovieCatalog();
catalog.addMovie(new Movie('Матрица', 1999, 8.7, 'Фантастика'));
catalog.addMovie(new Movie('Начало', 2010, 8.8, 'Фантастика'));
catalog.addMovie(new Movie('Форсаж 9', 2021, 4.5, 'Боевик'));
catalog.addMovie(new Movie('Крестный отец', 1972, 9.2, 'Драма'));
catalog.printAll();
// Матрица (1999) — 8.7/10
// Начало (2010) — 8.8/10
// Форсаж 9 (2021) — 4.5/10
// Крестный отец (1972) — 9.2/10
console.log(catalog.filterByGenre('Фантастика'));
// [Movie, Movie] — оба фантастических фильма
console.log(catalog.getTopRated());
// [Movie, Movie] — Начало и Крестный отец, у обоих rating >= 8.5Обратите внимание на связь с предыдущим уроком: методы filterByGenre и getTopRated внутри MovieCatalog устроены буквально на тех же filter, что мы разбирали как отдельные функции над массивом — только теперь они аккуратно спрятаны внутрь методов класса, управляющего своей собственной коллекцией объектов.
const movie = Movie('Матрица', 1999, 8.7); // ❌ TypeError: Class constructor Movie cannot be invoked without 'new'В отличие от обычных функций, классы в JavaScript требуют new при создании экземпляра — вызвать конструктор класса напрямую, как обычную функцию, невозможно; язык сам выбросит ошибку. Это отличается от Python, где Movie(...) без каких-либо специальных ключевых слов — стандартный и единственный способ создания экземпляра.
class Movie {
constructor(title, year) {
title = title; // ❌ Это просто присваивание параметру самому себе, свойство объекта не создаётся!
this.year = year;
}
}
const movie = new Movie('Матрица', 1999);
console.log(movie.title); // undefined — свойство title никогда не было создано на объекте
console.log(movie.year); // 1999 — а это сработало верно, потому что we написали this.yearБез this. слева от имени свойства присваивание происходит просто локальной переменной/параметру внутри функции-конструктора, а не свойству создаваемого объекта. Экземпляр в итоге останется без нужного свойства — распространённая ошибка по невнимательности, особенно когда имя параметра совпадает с именем свойства (что обычно и есть, поэтому ошибка легко проходит мимо взгляда).
Подробно разобрано в разделе 8 — если callback-функция внутри метода класса должна обращаться к this самого экземпляра, используйте стрелочную функцию, а не function.
const movie1 = new Movie('Матрица', 1999, 8.7);
const movie2 = new Movie('Матрица', 1999, 8.7);
console.log(movie1 === movie2); // false — разные объекты в памяти, несмотря на одинаковые данныеКак и с обычными объектами (Урок 18), === для экземпляров классов сравнивает ссылку, а не содержимое полей — даже полностью идентичные по данным экземпляры одного класса не будут равны через ===, если это не буквально одна и та же переменная.
class Movie {
static genreList = ['Фантастика', 'Драма'];
}
const movie = new Movie();
console.log(movie.genreList); // undefined — статическое свойство не "видно" через экземпляр
console.log(Movie.genreList); // ['Фантастика', 'Драма'] — только такРазобрано в разделе 5 — статические члены принадлежат самому классу, а не отдельным экземплярам, обращение к ним всегда идёт через имя класса.
Классы в JavaScript решают ту же задачу, что и в Python: описывают шаблон для создания однотипных объектов с общей структурой (свойства) и общим поведением (методы). Синтаксически ключевые соответствия: constructor — аналог __init__, this — аналог self, но с важным отличием — создание экземпляра в JS обязательно требует ключевого слова new.
Геттеры (get) и сеттеры (set) дают вычисляемые свойства, которые читаются без скобок вызова, — прямой аналог @property из Python. Статические члены (static) принадлежат самому классу, а не экземплярам — аналог @staticmethod/атрибутов класса. Приватные поля через # — это по-настоящему защищённые от внешнего доступа поля на уровне синтаксиса, в отличие от простого соглашения об именовании (_protected) в Python. Наследование (extends, super) работает по той же логике, что и в Python, с другим синтаксисом вызова родительского конструктора и методов.
Особое внимание стоит уделить поведению this внутри callback-функций, переданных методам массивов (forEach, map, filter) — обычная function создаёт собственный, несвязанный с классом this, тогда как стрелочная функция "наследует" this из окружающего кода. Это ключевая практическая причина, по которой в методах класса почти всегда стоит использовать именно стрелочные функции для callback-ов.
В следующем уроке — области видимости и замыкания: разберём подробнее, как именно JavaScript решает, к каким переменным у какого куска кода есть доступ, и почему var в циклах и функциях порой ведёт себя неожиданно.
- Какое ключевое синтаксическое отличие есть между созданием экземпляра класса в JavaScript и в Python?
- Что произойдёт, если внутри конструктора забыть написать
this.перед именем свойства? - Чем геттер (
get) отличается от обычного метода класса с точки зрения синтаксиса вызова? - Чем статический метод (
static) отличается от обычного метода класса? - В чём принципиальное отличие приватных полей через
#в JavaScript от соглашения об именовании через нижнее подчёркивание (_protected) в Python? - Почему внутри метода класса обычная
function, переданная вforEachкак callback, может привести к неожиданному поведениюthis? Как это решается? - Почему
movie1 === movie2вернётfalse, даже если оба объекта созданы одним и тем же классом с идентичными значениями всех полей?
Создайте класс Review со свойствами authorName, text, rating (число от 1 до 10), задаваемыми через конструктор.
- Добавьте геттер
shortText, возвращающий первые 50 символовtextс многоточием в конце, если текст длиннее 50 символов, иначе — текст целиком без изменений. - Добавьте метод
isPositive(), возвращающийtrue, еслиrating >= 7. - Добавьте сеттер для
rating, который выбрасывает ошибку (throw new Error(...)), если присваиваемое значение меньше 1 или больше 10 (используйте приватное поле#ratingвнутри, аналогично примеру в разделе 4 урока). - Создайте 2-3 экземпляра и проверьте работу геттера, метода и сеттера.
Создайте базовый класс Content со свойствами title, year и методом getSummary(), возвращающим "Название (год)".
- Создайте класс
Movie extends Contentс дополнительным свойствомduration(в минутах). ПереопределитеgetSummary(), чтобы добавить длительность через вызовsuper.getSummary(). - Создайте класс
Series extends Contentс дополнительным свойствомseasonsCount. ПереопределитеgetSummary(), чтобы добавить количество сезонов через вызовsuper.getSummary(). - Создайте по одному экземпляру каждого класса и выведите результат
getSummary()для обоих.
Создайте класс ReviewCatalog, управляющий коллекцией рецензий (используйте класс Review из Задания 1):
- Конструктор инициализирует пустой массив
this.reviews = []. - Метод
addReview(review)добавляет рецензию в массив. - Метод
getPositiveReviews()возвращает массив только положительных рецензий (isPositive() === true), используяfilter. - Метод
getAverageRating()возвращает средний рейтинг всех рецензий (используйтеreduce— можно посмотреть его в документации по аналогии сmap/filter, либо просто вручную сложить черезforEachи разделить на количество). - Метод
printAll()выводитshortTextиratingкаждой рецензии черезforEachсо стрелочной функцией.