Класс — это универсальный, комплексный тип данных, состоящий из тематически единого набора «полей» (переменных более элементарных типов) и «методов» (функций для работы с этими полями). Упрощенно класс можно представить в виде шаблона, в соответствии с которым создаются экземпляры класса.
Класс является моделью информационной сущности с внутренним и внешним интерфейсами для оперирования своим содержимым (значениями полей).
В частности, в классах широко используются специальные блоки из одного или чаще двух спаренных методов, отвечающих за элементарные операции с определённым полем (интерфейс присваивания и считывания значения), которые имитируют непосредственный доступ к полю. Другим проявлением интерфейсной природы класса является то, что при копировании соответствующей переменной через присваивание копируется только интерфейс, но не сами данные, то есть класс — ссылочный тип данных.
Переменная-объект, относящаяся к заданному классом типу, называется экземпляром класса. Обычно классы разрабатывают таким образом, чтобы обеспечить отвечающие природе объекта и решаемой задаче целостность данных объекта, а также удобный и простой интерфейс.
В свою очередь, целостность предметной области объектов и их интерфейсов, а также удобство их проектирования, обеспечивается наследованием или прототипированием.
В JavaScript классы используют прототипно-ориентированное наследование.
Часто на практике необходимо создавать много объектов одного вида (с одинаковым набором полей), например, пользователей, товары и т.д.
JavaScript содержит набор встроенных объектов, но вы можете создавать свои объекты следующими способами:
- с помощью функции-"конструктора" объекта new Object() (где Object - именование функции-"конструктора");
- с помощью конструктора экземпляра класса new Object() (где Object - именование класса class);
- с помощью фигурных скобок {…} с необязательным списком свойств (литеральная нотация).
Различают два варианта синтаксиса "class":
- class expression;
- class declaration.
Тело класса при использовании как class declaration, так и class expression будет исполняться в строгом режиме.
Методы в классе не разделяются запятой, это приводит к синтаксической ошибке.
Синтаксис "class expression"
Class expression - это способ определения класса в ECMAScript 2015 (ES6).
Схожий с function expressions, class expressions может быть:
- именованным (в этом случае его имя доступно только внутри класса);
- неименованным.
Синтаксис сlass expression:
1 2 3 |
var MyClass = class [className] [extends parentClass] { // ключевое слово extends может быть использовано для создания дочернего класса из parentClass // тело класса }; |
Особенности сlass expression:
- Можно опустить имя класса ([className] - "binding identifier"), что не допустимо в class declaration.
- Позволяет повторно объявить уже существующий класс и это не приведёт к ошибке типа, как при использовании class declaration.
- Свойство constructor() является опциональным.
- Результатом вызова оператора typeof на классах, сгенерированных при помощи class expression, всегда будет "function".
1 2 3 4 5 6 7 8 9 10 |
'use strict'; var MyClass = class {}; // свойство конструктора опционально var MyClass = class {}; // повторное объявление разрешено console.log(typeof MyClass); // возвращает "function" console.log(typeof class {}); // возвращает "function" console.log(MyClass instanceof Object); // true console.log(MyClass instanceof Function); // true class MyClass {} // SyntaxError: Identifier 'MyClass' has already been declared |
Пример простого анонимного class expression, на который можно сослаться с помощью переменной Foo (отсюда):
1 2 3 4 5 6 7 8 9 10 |
let Foo = class { constructor() {} bar() { return "Hello World!"; } }; let instance = new Foo(); console.log(instance.bar()); // "Hello World!" Foo.name; // "Foo" |
Пример именованного class expression (позволяет сослаться на конкретный класс внутри тела класса, имя будет доступно только внутри области видимости самого class expression):
1 2 3 4 5 6 7 8 9 10 |
let Foo = class NamedFoo { constructor() {} whoIsThere() { return NamedFoo.name; } }; let bar = new Foo(); console.log(bar.whoIsThere()); // "NamedFoo" console.log(Foo.name); // "NamedFoo" NamedFoo.name; // ReferenceError: NamedFoo is not defined (имя доступно только внутри области видимости самого class expression) |
Синтаксис "class declaration"
Синтаксис class declaration создаёт новый класс с данным именем на основе прототипного наследования.
В отличие от class expression, class declaration не позволяет снова объявить уже существующий класс, это приведёт к ошибке типа.
Синтаксис class declaration:
1 2 3 4 5 6 7 8 9 10 |
class nameClass [extends parentClass] { // ключевое слово extends может быть использовано для создания дочернего класса из parentClass prop = value; // свойство constructor(...) { // конструктор // ... } method(...) {} // метод [methodName]() {} // метод с вычисляемым именем get something(...) {} // геттер set something(...) {} // сеттер } |
nameClass технически является функцией (той, которую мы определяем как constructor()), в то время как методы, геттеры и сеттеры записываются в nameClass.prototype.
Свойство prop не устанавливается в nameClass.prototype. Вместо этого оно создаётся оператором new перед запуском конструктора, это именно свойство объекта.
Работа конструкции class declaration:
- Создаёт функцию с именем nameClass, которая становится результатом объявления класса.
- Код функции берётся из метода constructor() (она будет пустой, если такого метода нет).
- Сохраняет все методы в nameClass.prototype.
Особенности сlass declaration:
- Переопределение класса с помощью class declaration вызовет ошибку типа.
- Свойство constructor() является опциональным.
- Результатом вызова оператора typeof на классах, сгенерированных при помощи class expression, всегда будет "function".
Class declaration не поднимается (в отличие от декларируемых функций).
Пример простого class declaration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Polygon { constructor(height, width) { this.name = "Polygon"; this.height = height; this.width = width; } } class Square extends Polygon { // ключевое слово extends использовано для создания дочернего класса constructor(height, width) { // this.height; // ReferenceError, super должен быть вызван первым super(height, width); // Вызов метода конструктора родительского класса с длинами, указанными для ширины и высоты класса Polygon this.name = "Square"; } get area() { return this.height * this.width; } } let s = new Square(300,200); console.log(s); // 60000 |
В примере сначала определяется класс с именем Polygon, затем он наследуется для создания класса Square.
Оператор super() используется для вызова функций, принадлежащих родителю объекта. Может быть указан только в конструкторе constructor() и должен быть вызван до того, как будет использовано ключевое слово this.
Обратите внимание: super действует только внутри методов, например, greet: function() {} является "нормальным" свойством/функцией, а не методом, потому что он не следует синтаксису метода, и super выдаст ошибку (надо записать так: greet() {}, тогда super отработает без ошибок).
Попытка объявить класс дважды:
1 2 |
class Foo {}; class Foo {}; // Uncaught TypeError: Identifier 'Foo' has already been declared |
Та же ошибка будет вызвана, если класс уже был определён перед использованием class declaration:
1 2 |
var Foo = class {}; class Foo {}; // Uncaught TypeError: Identifier 'Foo' has already been declared |
Класс как аналог функции-конструктора
1 2 3 4 5 6 7 8 9 10 |
class Polygon { constructor(height, width) { this.name = "Polygon"; this.height = height; this.width = width; } } let newPoligon = new Polygon(3, 5); console.log(newPoligon); // Polygon {name: "Polygon", height: 3, width: 5} |
Перепишем класс Polygon как функцию-конструктор:
1 2 3 4 5 6 7 8 |
function Polygon(height, width) { this.name = "Polygon"; this.height = height; this.width = width; } let newPoligon = new Polygon(3, 5); console.log(newPoligon); // Polygon {name: "Polygon", height: 3, width: 5} |
Отличия классов от функций-конструкторов:
- Функция, созданная с помощью class, помечена специальным внутренним свойством [[FunctionKind]]:"classConstructor".
- В отличие от обычных функций, конструктор класса вызывается с помощью new.
- Методы класса являются неперечислимыми (определение класса устанавливает флаг enumerable в false для всех методов в prototype, поэтому, например, цикл for...in их не переберёт).
- Классы всегда используют 'use strict' (весь код внутри класса автоматически исполняется в строгом режиме).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Функция-конструктор класса также вызывается с помощью new function User(userName) { console.log(userName); } // вызов обычной функции let newUser = User("Alex"); // Alex console.log(newUser); // undefined (т.к. функция не содержит return, т.е. ничего не возвращает) // вызов функции-конструктора newUser = new User("Alex"); // Alex console.log(newUser); // User {} (функция-конструктор вернула новый объект User) |
Создание классов динамически («по запросу»)
Допускается динамическое (по запросу, во время выполнения кода) создание классов:
1 2 3 4 5 6 7 8 9 10 11 12 |
function makeClass(phrase) { // объявляем класс и возвращаем его return class { sayHi() { console.log(phrase); }; }; } // Создаём новый класс let NewUser = makeClass("Привет"); console.log(NewUser) // class {...} new NewUser().sayHi(); // Привет |
Get и set (геттеры и сеттеры) в классах
Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д., например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Name { constructor(name) { this.name = name; // вызывает сеттер (set name) } get name() { return this._name; } set name(value) { if (value.length < 4) { console.log("Имя слишком короткое."); return; } this._name = value; } } let user = new Name("Иван"); console.log(user.name); // Иван (читаем через геттер) user.name = "Mary"; // устанавливаем через сеттер console.log(user.name); // Mary user = new Name(""); // Имя слишком короткое. |
После объявления класса геттеры/сеттеры добавляются в User.prototype, например:
1 2 3 4 5 6 7 8 9 10 |
Object.defineProperties(User.prototype, { name: { get() { return this._name; }, set(name) { // ... }, }, }); |
Пример с вычисляемым свойством в квадратных скобках [...]:
1 2 3 4 5 6 7 |
class Greeting { ["say" + "Hello"]() { console.log("Привет"); } } new Greeting().sayHello(); // Привет |