- Что такое классы?
- Пример объявления класса
- Создание экземпляров класса
- Вычисляемые свойства
- Наследование и цепочка прототипов
- Расширение классов
- Переопределение методов
- Расширение встроенных классов
- Приватные свойства и методы
- Геттеры и сеттеры
- Статические свойства и методы
- Наследование статических свойств и методов
- Комментарии
Всё о ES6 классах в JavaScript

В этой статье мы изучим всё что касается ES6 классов в JavaScript. Разберём синтаксис создания классов посредством class
, научимся создавать экземпляры классов, рассмотрим как расширять классы, что такое статические свойства и методы, и многое другое.
Что такое классы?
Начиная с ECMAScript 2015 (ES6) в языке JavaScript появился синтаксис классов. В этом синтаксисе вы можете создавать классы, используя ключевое слово class
.
А как же дела обстояли до этого стандарта? Как создавались классы? Их просто не было, так как JavaScript не является классическим объектно-ориентированным языком. Создание объектов осуществлялось с помощью конструкторов или, другими словами, посредством обычных функций, которые вызывались с использованием ключевого слова new
.
Под классом в JavaScript мы будем понимать шаблон, на основании которого создаются объекты. Чтобы не путаться, классом будем считать как шаблон, созданный с помощью конструктора, так и с помощью ключевого слова class
:
function Person() {}
class Student {}
В этом примере мы создали два класса Person
и Student
.
Что же с наследованием? В JavaScript имеется только прототипное наследование и работает оно через объекты. С приходом классов в этом плане ничего не изменилось.
Так что же такое class
? class
– это специальная функция, предназначенная для создания объектов и организации существующего в языке прототипного наследования. То есть class
– это просто более новый способ написания шаблона с большим количеством крутых возможностей, который затем можно использовать для создания нужного количества однотипных объектов.
Кстати, проверить, что class
является функцией можно с помощью оператора typeof
:
class Car {}
typeof Car // 'function'

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

Таким образом с помощью одного класса (одной заготовки) мы можем создать много однотипных объектов. При этом они будут независимы друг от друга и иметь как свои свойства и методы, так и наследуемые.
Создание класса начинается с ключевого слова class
после которого идёт название класса, написанное в нотации PascalCase (первая буква заглавная). Далее между фигурными скобками находится всё что касается этого класса, то есть свойства и методы.
Пример создания класса Post
:
class Post {
constructor(text) {
this.text = text;
this.totalLikes = 0;
}
addLike() {
this.totalLikes += 1;
}
}
В этом классе находятся 2 метода: constructor
и addLike
. Синтаксис методов очень простой: сначала указывается название метода, далее в круглых скобках при необходимости параметры, а затем его тело в фигурных скобках.
constructor
– это специальный метод, который вызывается только при создании новых объектов, в данном случае экземпляров класса Post
. В этом методе описываются свойства и методы, которые необходимо непосредственно добавить в создаваемый объект.
В данном примере у конструктора имеется один параметр text
. При вызове Post
мы можем передать значение этому параметру, то есть аналогично тому, как мы это делаем при вызове функций.
В конструкторе имеется переменная this
. Это специальная переменная, которая ссылается на создаваемый объект. То есть, когда вы будете создавать новый экземпляр класса, this
автоматически будет указывать на него. С помощью this
мы добавляем к этому объекту сначала свойство text
со значением, которое мы передали в вызов Post
. После этого ещё одно свойство totalLikes
но уже со значением 0
.
addLike
– это метод, посредством которого мы будем увеличивать количество лайков у конкретного объекта типа Post
. addLike
находится не в самих объектах, а на уровне класса Post
, то есть в Post.prototype
. Все создаваемые объекты будут иметь доступ к нему посредством наследования.

Почему так? Потому что addLike
мы описали вне конструктора. А все методы, описанные вне его, будут находиться на уровне класса, то есть в свойстве prototype
.
Откуда взялось это свойство prototype
? Это свойство появляется автоматически при объявлении класса. Это прототип, который в дальнейшем будет устанавливаться для всех объектов, создаваемых посредством этого класса. Используются прототипы для наследования. То есть для того чтобы созданные объекты имели доступ к свойствам и методам, расположенных в прототипах.
В данном примере объект Post.prototype
имеет 2 метода: constructor
и addLike
.
Свойство constructor
в прототипе содержит сам класс Post
:
Post.prototype.constructor === Post // true
Итак, мы создали класс Post
. У него имеется 2 метода: constructor
и addLike
. Конструктор вызывается только при создании нового объекта. Метод addLike
будет наследоваться и доступен для каждого экземпляра класса Post
.
Создание экземпляров класса
Создание нового экземпляра класса выполняется очень просто с помощью ключевого слова new
:
const firstPost = new Post('Какой-то текст');
В этом примере мы переменной firstPost
присвоили новый объект, который создали с помощью синтаксиса new Post('Какой-то текст')
. Здесь мы вызвали функцию Post
посредством указания после неё круглых скобок, а в них передали значение для параметра text
. В результате вызовется constructor
и ему передадутся на вход эти аргументы. В данном случае один аргумент. На выходе мы получим новый объект с указанными свойствами и методами, и который будет иметь в качестве прототипа объект Post.prototype
.
Выведем значение переменной в консоль:
console.log(firstPost);

Здесь вы видите собственные свойства объекта. Кроме этого, если раскрыть специальное внутреннее свойство [[Prototype]]
, то мы увидим прототип, а в нём метод addLike
. Этот метод находится на уровне класса Post
и наследуется всеми его экземплярами.
Соответственно, мы можем вызвать addLike
как метод переменной firstPost
.
firstPost.addLike();
console.log(firstPost.totalLikes); // 1
firstPost.addLike();
console.log(firstPost.totalLikes); // 2

Когда мы вызываем addLike
, this
внутри этого метода будет указывать на объект, который стоит перед точкой. В данном случае this
– это будет firstPoint
. При первом вызове firstPost.addLike()
мы увеличили значение свойства totalLikes
объекта firstPost
на 1
. После второго вызова до 2.
Таким образом не имеет смысла добавлять метод addLike
каждому экземпляру класса Post
. Достаточно чтобы он наследовался, а для этого нужно просто его добавить в прототип.
С помощью класса можно создать много однотипных объектов:
const secondPost = new Post('Другой текст');
const thirdPost = new Post('Иной текст');
Вот как раз для этого и нужны классы, чтобы мы могли посредством них очень просто делать множество похожих объектов. При этом у каждого из них будут свои собственные свойства и методы.
Обратите внимание, что конструктор класса Post
нельзя вызвать без new
:
const fourthPost = Post('Четвёртый пост'); // Uncaught TypeError: Class constructor Post cannot be invoked without 'new'
Вычисляемые свойства
В классе, также как в литерале объекта, можно объявлять вычисляемые свойства. Вычисляемое свойство – это такое, у которого в качестве имени используется значение, полученное в результат вычисления указанного в квадратных скобках выражения.
const PREFIX = 'itc';
class ShoppingList {
[PREFIX + 'Products'] = [];
[PREFIX + 'Add'](item) {
this[PREFIX + 'Products'].push(item);
}
}
const day1 = new ShoppingList();
day1[PREFIX + 'Add']('Огурцы 1 кг');
day1[PREFIX + 'Add']('Помидоры 0.5 кг');
console.log(day1[PREFIX + 'Products']);
Так как здесь нам не нужно получать аргументы, то мы опустили конструктор. Но если они нужны, то без конструктора не обойтись. В этом случае необходимо добавить constructor
и перечислить в круглых скобках параметры. После этого при создании нового объекта вызовется конструктор и его параметры получат значения, которые мы передали в вызов класса посредством аргументов.
Свойства, объявленные вне конструктора, в отличие от методов не устанавливаются в prototype
. Они создаются как собственные свойства объектов перед запуском конструктора.
Наследование и цепочка прототипов
Что такое наследование? Если после переменной firstPost
поставить точку, то вы увидите все доступные для этого объекта свойства и методы. Здесь будут как собственные свойства и методы этого объекта, так и наследуемые.

Откуда всё это взялось? Всё из-за того, что объект firstPost
имеет ссылку на прототип Post.prototype
, а он в свою очередь на свой прототип и так далее. В результате для firstPost
получается следующая цепочка прототипов:
firstPost -> Post.prototype -> Object.prototype
Наследование в JavaScript как вы уже знаете осуществляется через прототипы. Таким образом firstPost
имеет доступ ко всем этим свойствам и методам. То есть ему доступны не только его свойства и методы, но и те, которые находятся в прототипах Post.prototype
и Object.prototype
.
Полезные методы:
1. hasOwnProperty
. С помощью hasOwnProperty
можно проверить принадлежность того или иного свойства конкретному объекту:
firstPost.hasOwnProperty('text') // true
firstPost.hasOwnProperty('addLike') // false
Здесь мы проверяем, есть ли у объекта firstPost
собственное свойство, которое называется text
. В данном случае мы получаем true
. Но при проверке названия addLike
, мы получили false
. Потому что addLike
это не собственный метод объекта firstPost
, он находится в Post.prototype
.
2. Object.hasOwn
. Это статический метод, который появился в стандарте ECMAScript 2022. Он позволяет проверить, есть ли у объекта собственное свойство, то есть делает тоже самое что hasOwnProperty
:
Object.hasOwn(firstPost, 'text') // true
Object.hasOwn(firstPost, 'addLike') // false
Object.hasOwn
рекомендуется использовать вместо hasOwnProperty
.
3. instanceof
. Оператор instanceof
позволяет проверить принадлежность объекта тому или иному классу с учетом цепочки прототипов:
firstPost instanceof Post // true
firstPost instanceof Object // true
firstPost instanceof Date // false
На второй строке мы тоже получаем true
, потому что Object
имеется в цепочке прототипов объекта firstPost
.
4. isPrototypeOf
. С помощью isPrototypeOf
мы можем проверить, существует ли объект в цепочке прототипов другого объекта:
Post.prototype.isPrototypeOf(firstPost);
То есть он проверяет, есть ли Post.prototype
где-то в прототипной цепочке объекта firstPost
.
Расширение классов
В JavaScript мы можем создать дочерний класс, объекты которого будут наследовать прототип родительского класса. Осуществляется это в JavaScript с использованием ключевого слова extends
.
Допусти у нас имеется класс Point2D
:
class Point2D {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x},${this.y})`;
}
}
Например, мы хотим, чтобы Point3D
расширял класс Point2D
. Для этого необходимо использовать ключевое слово extends
, после которого указать название родительского класса:
class Point3D extends Point2D {
constructor(x, y, z) {
super(x, y);
this.z = z;
}
toString() {
return `(${this.x},${this.y},${this.z})`;
}
}
Ключевое слово extends
по факту устанавливает в качестве прототипа Point3D.prototype
объект Point2D.prototype
, то есть следующее:
Point3D.prototype.__proto__ = Point2D.prototype

В Point3D
мы переопределяем конструктор родительского класса. Поэтому перед тем, как обращаться к this
нам необходимо сначала вызвать constructor
родительского класса. Осуществляется это в JavaScript с помощью ключевого слова super
:
constructor(x, y, z) {
super(x, y);
this.z = z;
}
В круглых скобках мы передали аргументы для параметров x
и y
конструктору родительского класса. После этого с помощью this
добавили новое свойство z
в объект. Затем мы переопределили метод toString
, теперь он будет использоваться взамен родительского:
toString() {
return `(${this.x},${this.y},${this.z})`;
}
Создадим новый экземпляр класса Point3D
:
const point = new Point3D(5, 7, 3);
Этот объект будет иметь следующую цепочку прототипов:
point -> Point3D.prototype -> Point2D.prototype -> Object.prototype

Вызовем метод toString
:
point.toString() // '(5,7,3)'
Обратите внимание, что после extends
допускается использовать выражение:
class Point3D extends (() => Point2D)() {}
Кстати, классы в JavaScript являются выражениями. То есть их можно присваивать переменным, передавать в качестве аргументов другим функциям и так далее.
Например:
// класс присвоили переменной Drink
const Drink = class {
constructor(price, discount = 1) {
// округлим до двух знаков после запятой
this.price = Math.round(price * discount * 100) / 100;
}
}
// присвоим переменной juice созданный объект класса Drink
const juice = new Drink(1.25);
// функция, в которую будем передавать класс и скидку
const drinkWithDiscount = (Base, discount) => {
// в качестве результата возвращаем новый класс, расширенный от Base с указанной скидкой
return class extends Base {
constructor(price) {
super(price, discount);
}
}
}
// переменной Tea присвоим класс, который будет возвращать функция drinkWithDiscount
const Tea = drinkWithDiscount(Drink, 0.8);
// создадим объекты, являющиеся экземплярами класса Tea
const greenTea = new Tea(2);
const blackTea = new Tea(1.5);
Переопределение методов
В примере, приведённом выше, мы в дочернем классе определили свой метод toString
. В данном случае для экземпляров этого класса будет использоваться именно он.
Но нам не всегда нужно полностью переопределять родительский метод. Допустим мы хотим в новом методе просто дописать функционал, которого нет в родительском методе. Тогда чтобы не дублировать код родительского метода мы можем его просто вызвать в дочернем методе. Осуществляется этого в JavaScript с использованием super
.
Например:
class Rectangle {
constructor(a, b) {
this.a = a;
this.b = b;
}
getArea() {
return this.a * this.b;
}
}
class RightTriangle extends Rectangle {
getArea() {
return super.getArea() / 2;
}
}
const triangle = new RightTriangle(5, 10);
console.log(triangle.getArea()); // 25
В RightTriangle
мы переопределили родительский метод getArea
. Для вычисления площади прямоугольного треугольника нам в данном случае можно просто вызвать родительский метод getArea
и разделить возвращённое им значение на 2
. Вызов родительского метода в JavaScript как уже отмечали выше осуществляется с помощью super
:
super.getArea() / 2
Обратите внимание, что в расширяемом классе RightTriangle
нет собственного конструктора. Когда в дочернем классе нет конструктора, то используется конструктор родительского класса:
То есть по факту когда нет метода constructor
в дочерней класс автоматически добавляется следующее:
class RightTriangle extends Rectangle {
// конструктор, который будет вызывать родительский конструктор
constructor(...args) {
super(...args);
}
// ...
}
Рассмотрим ещё один очень интересный пример с переопределением метода:
class Square {
constructor(left, top, side) {
this.constructor.counter++;
this.id = `square-${this.constructor.counter}`;
this.left = left;
this.top = top;
this.side = side;
}
draw() {
document.body.insertAdjacentHTML('beforeend', `<div id="${this.id}" style="position: absolute; top: ${this.top}px; left: ${this.left}px; width: ${this.side}px; height: ${this.side}px; background-color: black;"></div>`);
}
static counter = 0;
}
class ColorSquare extends Square {
constructor(left, top, side, color) {
super(left, top, side);
this.color = color;
}
draw() {
super.draw();
document.querySelector(`#${this.id}`).style.backgroundColor = this.color;
}
}
const square1 = new Square(100, 100, 50);
square1.draw();
const square2 = new ColorSquare(150, 200, 75, 'green');
square2.draw();
Здесь в дочернем классе мы объявили метод draw
и тем самым переопределили метод родительского класса. В draw
мы сначала вызываем родительский super.draw()
, а затем получаем этот элемент на странице и изменяем его цвет.
Расширение встроенных классов
Создавать дочерние классы можно также от встроенных, которые присутствуют в JavaScript по умолчанию.
Например, расширим Array
и добавим в новый класс метод clear
, который будет очищать массив от нежелательных значений, таких как undefined
, null
, ''
и так далее:
class ExtArray extends Array {
clear() {
return this.filter((el) => el);
}
}
const arr = new ExtArray(3, null, 7, undefined, '');
console.log(arr.clear()); // [3, 7]
В этом примере мы не стали явно добавлять constructor
, так как здесь нам не нужно каким-то образом расширять функционал родительского конструктора. В этом случае при создании нового экземпляра этого класса как мы уже отмечали выше просто вызовется родительский конструктор.
После создания нового объекта класса ExtArray
, нам будет доступен новый метод clear
. Но, кроме этого, ещё все методы массивов и объектов. Происходит это благодаря наследованию. Объект arr
имеет следующую цепочку прототипов:
arr -> ExtArray.prototype -> Array.prototype -> Object.prototype
Приватные свойства и методы
До этого мы создавали только публичные свойства и методы. Мы к ним имеем доступ как внутри класса, так и снаружи.
Кроме публичных мы можем ещё объявлять приватные свойства и методы, которые будут доступны только внутри класса. То есть к ним нельзя обратиться извне, в том числе и из дочерних классов.
Указание свойству или методу того, что он является приватным осуществляется посредством специального символа #
:
class Person {
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
getName() {
return this.#name;
}
getAge() {
return this.#age;
}
setAge(value) {
if (Number.isInteger(value) && value > 0) {
this.#age = value;
}
throw new Error('Ошибка при установке значения свойству #age! Значение не является целым положительным числом.');
}
}
Создание объекта john
:
const john = new Person('John', 27);

Для организации доступа к приватным свойствам (#name
, #age
) вместо обычных методов удобно использовать геттеры и сеттеры.
Геттеры и сеттеры
Геттеры и сеттеры – это специальные методы, доступ к которым осуществляется через связанное с каждым из них свойство. При этом геттер работает при чтении свойства, а сеттер – при присвоении свойству некоторого значения. При написании геттера, его необходимо задавать соответственно без параметров, а сеттер – с одним параметром.
Другими словами, геттер и сеттер позволяют создать на вид обычное свойство, но работающее через функции.
Очень часто геттеры и сеттеры применяются соответственно для чтения приватных свойств и присвоения им новых значений:
class Person {
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
// геттер для получения имени
get name() {
return this.#name;
}
// геттер для получения возраста
get age() {
return this.#age;
}
// сеттер для установки возраста
set age(value) {
if (Number.isInteger(value) && value > 0) {
return this.#age = value;
}
throw new Error('Ошибка при установке значения свойству #age! Значение не является целым положительным числом.');
}
}
В этом примере нельзя после создания объекта изменить свойство #name
, а присвоение нового значения #age
осуществляется только после его успешной проверки.
Для этого мы свойства #name
и #age
сделали приватными. После этого доступ к свойствам #name
и #age
реализовали через геттеры. Геттеры отличаются от методов только наличием перед ними ключевого слова get
.
Так как #name
изменять не нужно, то сеттер написали только для #age
. Сеттер начинается с set
, затем идёт имя и круглые скобки, в которых обязательно указываем параметр с помощью которого мы будем получать значение для его установки свойству #age
.
Как вы уже поняли, свойство, связанное с сеттером или геттером определяется его именем. В этом примере – это name
и age
.
Создадим объект:
const tom = new Person('Tom', 25);
// получаем имя с помощью сеттера name
console.log(tom.name);
// установим возраст с помощью геттера age
tom.age = 27;
// получим возраст с помощью сеттера age
console.log(tom.age);
// попробуем установить некорректное значение посредством сеттера age
tom.age = -26;

В этом примере видно что сеттеры и геттеры выглядят как свойства, но в классе они являются методами. Например, сеттер для установки #age
проверяет значение. Если оно имеет отрицательную величину, то выбрасывается исключение с помощью throw
.
По сути, при объявлении класса геттеры и сеттеры записываются в Person.prototype
вот так:
Object.defineProperties(Person.prototype, {
name: {
get() { /* ... */ }
},
age: {
get() { /* ... */ },
set(value) { /* ... */ }
}
}
Рассмотрим ещё один очень интересный пример. В нём посредством сеттеров и геттеров мы будем соответственно сразу получать несколько свойств и устанавливать им новые значения:
class Point {
constructor(x, y) {
[this.x, this.y] = [x, y];
}
get xy() {
return [this.x, this.y];
}
set xy(value) {
[this.x, this.y] = value;
}
}
const point = new Point(2, 4);
point.xy = [3, 8];
console.log(point.xy); // [3, 8]
Как вы уже поняли, основное назначение геттеров и сеттеров – это возможность добавить необходимую нам логику, когда происходит чтение и запись свойств.
Свойство, определённое с помощью геттеров и сеттеров находится в прототипе объекта:
Object.getOwnPropertyDescriptor(point, 'xy');
Object.getOwnPropertyDescriptor(point.__proto__, 'xy');

Статические свойства и методы
Статические свойства и методы – это такие, которые принадлежат самому классу, а не объектам, созданным на его основе. То есть они доступны как свойства и методы этого класса и не наследуются его экземплярами.
Создаются статические свойства и методы с использованием ключевого слова static
:
class Post {
constructor(text, date) {
this.text = text;
this.date = date;
this.totalLikes = 0;
Post.contains.push(this);
}
addLike() {
this.totalLikes += 1;
}
// статическое свойство
static contains = [];
// статический метод
static create(text) {
return new this(text, new Date().toLocaleDateString());
}
}
В этом примере у нас имеется:
- статическое свойство
contains
, в которое мы будем записывать созданные посты; - статический метод
create
для создания нового экземпляр классаPost
с текущей датой.
Статические свойства и методы принадлежат самому классу, поэтому обращаться к ним необходимо как к свойствам и методам самого класса. В этом и заключается отличие статических и наследованных свойств и методов. Почему это возможно? Потому что класс – это специальная функция, а функция – это объект.
Вызов статического метода осуществляется таким способом:
// вызовем статического метода
Post.create('Какой-то текст');
// получим количество созданных постов посредством статического свойства
console.log(Post.contains.length);

Выведем список свойств класса Post с помощью console.dir
:

Метод console.dir
позволяет нам увидеть класс Post
как объект.
Теперь давайте выведем в консоль Post.prototype
:

В этом объекте, который является прототипом для всех экземпляров класса Post
статических свойств и методов нет. Но если мы раскроем constructor
, то найдём их. То есть статические свойства и методы находятся в конструкторе и не доступны объектам, являющимися экземплярами класса Post
.
Наследование статических свойств и методов
В JavaScript при расширении класса посредством extends наследуются его статические свойства и методы. То есть расширенный класс имеет доступ к статическим свойствам и методам родительского класса:
Рассмотрим пример:
class Animal {
constructor(name) {
this.name = name;
this.type = this.constructor.name.toLowerCase();
let contains = this.constructor.contains[this.type];
contains = contains ?? [];
contains.push(this);
}
speak() {
console.log(`I am ${this.type}`);
}
static contains = {};
static getContainsByType() {
const type = this.name. toLowerCase();
return this.contains[type];
}
}
class Dog extends Animal {}
class Cat extends Animal {}
В этом примере у нас имеются три класса Animal
, Dog
и Cat
. В Animal
у нас имеется статическое свойство contains
и метод getContainsByType
. Классы Dog
и Cat
мы создали посредством расширения родительского класса Animal
.
Как работает наследование статических свойств и методов? Как обычно с использованием прототипов. То есть, когда мы, например, создаём класс Dog посредством расширения Animal
, то у нас просто создаются 2 ссылки:
- для экземпляров класса
Dog
:Dog.prototype.__proto__ = Animal.prototype
; - для статических свойств и методов:
Dog.__proto__ = Animal
.
Теперь создадим объекты и проверим наследование статических свойств и методов:
// создадим объекты классов Dog и Cat
new Dog('Несси');
new Dog('Оскар');
new Cat('Пушок');
// выведем в консоль contains как свойство Dog
console.log(Dog.contains);
// вызовем getContainsByType как метод Dog
Dog.getContainsByType();
Но наследование статических свойств и методов характерно только при использовании extends
. Например, встроенные классы (Date
, Array
и другие) не наследуют статические методы родительского класса.