Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Классы JS: Программирование, управляемое данными

--

Большинство объектно-ориентированных языков содержат в себе специальную структуру, которая позволяет создавать новые объекты и такая структура называется class.

Генерация объектов

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

export const make = (name, percent) => {
  return {
    name: name,
    damage: health =>
      Math.round(health * (percent / 100)),
  }
};

У нас была просто функция, которая внутри себя создаёт объект, как тип данных и возвращает его наружу.

Видно, что по сути это некий генератор объектов. При этом есть специальные механизмы для того, чтобы делать это встроенными средствами языка, а не самостоятельно. Кода будет больше, но этот способ обладает определёнными преимуществами. Такой способ называется – класс.

Класс

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

Во-вторых, обычно принято (это не только в JS), что у вас один файл – это определение одного типа. Будет крайне странно определять несколько типов в одном файле и по этой причине нет смысла никогда экспортировать класс по имени, его экспортируют по умолчанию. То есть у вас один класс сразу представляет из себя какой-то конкретный тип, который вы импортируете к себе и получаете возможность создавать объекты данного типа.

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

Синтаксис

// CamelCase
export default class PercentCard {
  constructor(name, percent) {
    this.name = name;
    this.percent = percent;
  }

  damage(health) {
    return Math.round(health * (this.percent / 100));
  }
}

После того, как идёт специальное имя class и название класса открываются фигурные скобки и дальше мы внутри перечисляем разные механизмы.

Внутри нашего класса определено 2 функции и делается это следующим образом: мы просто пишем имя, после этого скобки, в скобках какие-то аргументы (если они есть) и дальше идёт тело в блоке инструкций. Как видите никаких дополнительных запятых и каких-то знаков нет. Синтаксис немножко похож на то, как создаются объекты в том смысле, что используются фигурные скобки, но внутри мы не используем ключ: значение.

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

Первая функция, которая у нас определяется в классе называется конструктор:

constructor(name, percent) {
  this.name = name;
  this.percent = percent;
}

Мы уже знаем, что такое конструктор и зачем он нужен. Он принимает два параметра, которые мы обычно использовали для карты этого типа: name и percent. Внутри они записываются, как this.name = name и this.percent = percent.

Здесь мы видим нечто новое. Исходя из того, что здесь есть точка, можно сделать вывод, что this – это объект, а исходя из названия, что этот объект указывает на себя, то есть на экземпляр текущего класса (на самом деле так и есть). Мы чуть позже посмотрим, что такое this и как он работает.

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

this сюда никак не передаётся, он каким-то образом здесь появляется, как и в конструкторе.

this как пара

Давайте вспомним, как работали в таком же стиле, но с парами, когда у нас был конструктор и какие-то функции:

import { cons, car, cdr } from 'hexlet-pairs';

export const make = (name, percent) =>
  cons(name, percent);

export const getName = (self) => car(self);

export const damage = (self, health) =>
  Math.round(health * (cdr(self) / 100));

// using

const card = make('name', 80);
getName(card); // name

https://repl.it/@hexlet/js-ddp-classes-this-as-a-pair

Если мы говорим про конструктор make, то здесь конечно не пахнет никаким this, мы просто делаем пару и возвращаем её наружу. Но, если мы посмотрим все остальные функции, которые мы использовали для данного типа данных, то мы вспомним, что первым параметром всегда шёл текущий экземпляр с которым мы работаем. И в последних уроках мы начали его называть self.

Почему не this?

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

Получается, что на самом деле первым параметром всегда передаётся сам объект, с которым мы работаем. Это очень важно запомнить, поскольку позже это пригодится.

this как объект

Изменим предыдущий пример с пар на объект:

export const make = (name, percent) => {
  return { name: name, percent: percent };
};

export const getName = (self) => self.name;

export const damage = (self, health) =>
  Math.round(health * (self.percent / 100));

// using

const card = make('name', 80);
getName(card); // name

https://repl.it/@hexlet/js-ddp-classes-this-as-a-object

Внутри make мы возвращаем объект в стиле { name: name, percent: percent }. Обратите внимание, что при использовании функций мы внутри берём этот self и обращаемся к нему, как self.name и self.percent. И это уже очень сильно похоже на то, как происходит работа внутри класса. И на самом деле можно сказать, что это одно и тоже.

this в классе

export default class PercentCard {
  constructor(<this>, name, percent) { // this === {}
    this.name = name;
    this.percent = percent;
  }

  damage(<this>, health) {
    return Math.round(health * (this.percent / 100));
  }
}

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

Если точнее, то this передаётся не в класс, а в конкретный объект, который представляет собой вот этот тип данных (не забываем, что класс – это про типы данных, а не про объекты).

Когда мы работаем с конкретным объектом, то передаётся this, который мы наполняем и после этого он также попадает в каждую функцию (опять же нулевым аргументом, это происходит неявно, за нас это делает движок JS) и внутри мы можем им пользоваться.

При этом когда он попадает в конструктор, то можно представить себе, что this – это пустой объект. Это не совсем так, но для начала это совершенно нормально, если мы себе это будем представлять именно так.

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

Экземпляр класса

Теперь разберёмся, как работать с классами и как создать объект.

typeof PercentCard; // function

const card = new PercentCard('card name', 90);
// constructor(name, percent) {
//  this.name = name;
//  this.percent = percent;
// }

card.name; // card name
card.percent; // 90
card.damage(10); // 9

card.wrongKey; // undefined

card['name']; // card name

https://repl.it/@hexlet/js-ddp-classes-class-instance

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

class Fake {
}

const obj = new Fake();

Синтаксис здесь чуть-чуть новый, добавляется одно ключевое слово new и есть один маленький нюанс касающийся него.

На самом деле new в JS работает совсем не так, как в большинстве других языков программирования, то есть тут уже врывается специфика. На самом деле классы в JS, как синтаксическая конструкция, появились только с ES6 (это один из последних стандартов JS), но до этого их не было. И классы, по сути, не привнесли в язык ничего нового с точки зрения внутреннего устройства.

Класс – это просто синтаксический сахар над системой прототипов, о которой мы пока не говорим. Сейчас мы просто попытаемся понять, что означает new и как это работает.

Когда мы создаём класс у нас имя PercentCard становится занятым. С этим идентификатором мы можем что-то сделать, потому что всё является объектом первого рода. Если мы вызовем функцию typeof на имени нашего класса, то для многих станет неожиданностью, что его тип будет function. То есть на самом деле наш класс – это функция.

Классов в реальности в JS не существует, но их добавили по той простой причине, что так проще программировать. Большинство людей привыкло именно к такому подходу и в большинстве языков существует именно понятие класс.

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

Если мы попытаемся создать инстанс класса или экземпляр класса (какой-то объект из этого класса), то мы вызываем эту функцию и тогда становится понято почему это функция, (это именно вызов функции) в которую мы передаём параметры.

Возникает вопрос. Что это за функция?

На самом деле эта функция — это наш конструктор, который здесь закомментирован:

const card = new PercentCard('card name', 90);
// constructor(name, percent) {
//  this.name = name;
//  this.percent = percent;
// }

Тогда зачем нужен new, если это просто функция, которая могла бы это сделать?

Если обратить внимание на устройство конструктора, то можно заметить, что этот this мы не возвращаем наружу, мы вообщем не делаем никаких возвратов и в том числе по этой причине нужен new. То есть это такая операция, которая делает дополнительную работу за нас. Сейчас мы не будем говорить, как она устроена, потому что происходит несколько шагов, которые опять же связаны с прототипами. Самое главное, что в конце концов она прокидывает в конструктор специальный this, который возвращается нам наружу и поэтому получается, что внутри card оказывается тот же самый объект, с которым мы работали раньше (уже как тип данных объект в языке).

Теперь работа никак не отличается от предыдущих примеров:

typeof PercentCard; // function

const card = new PercentCard('card name', 90);
// constructor(name, percent) {
//  this.name = name;
//  this.percent = percent;
// }

card.name; // card name
card.percent; // 90
card.damage(10); // 9

card.wrongKey; // undefined

card['name']; // card name

Например, мы вызываем card.name и получаем имя карты.

Как мы видим внутри класса для этого ничего не нужно специально определять, потому что это просто обычный объект когда мы делаем его инстанс и поэтому this.name достаточно для того, чтобы потом снаружи получить этот name. То же самое касается процента, теперь он у нас доступен и мы можем обращаться ко всему, что внутри было установлено, как this.something. Аналогично с функцией damage ничего в плане синтаксиса не изменилось. Также не нарушается правило связанное с несуществующими ключами: мы вызываем wrongKey и получаем undefined. Ну и конечно здесь будет работать тот же самый синтаксис обращения через квадратные скобки вместо точечной нотации.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»