В современном JavaScript появился новый, «более красивый» синтаксис для классов.
Новая конструкция class
– удобный «синтаксический сахар» для задания конструктора вместе с прототипом.
Class
Синтаксис для классов выглядит так:
class Название [extends Родитель] {
constructor
методы
}
Например:
'use strict';
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
let user = new User("Вася");
user.sayHi(); // Вася
Функция constructor
запускается при создании new User
, остальные методы записываются в User.prototype
.
Это объявление примерно аналогично такому:
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
alert(this.name);
};
В обоих случаях new User
будет создавать объекты. Метод sayHi
также в обоих случаях находится в прототипе.
Но при объявлении через class
есть и ряд отличий:
User
нельзя вызывать безnew
, будет ошибка.- Объявление класса с точки зрения области видимости ведёт себя как
let
. В частности, оно видно только в текущем блоке и только в коде, который находится ниже объявления (Function Declaration видно и до объявления).
Методы, объявленные внутри class
, также имеют ряд особенностей:
- Метод
sayHi
является именно методом, то есть имеет доступ кsuper
. - Все методы класса работают в строгом режиме
use strict
, даже если он не указан. - Все методы класса не перечислимы. То есть в цикле
for..in
по объекту их не будет.
Class Expression
Также, как и Function Expression, классы можно задавать «инлайн», в любом выражении и внутри вызова функции.
Это называется Class Expression:
'use strict';
let User = class {
sayHi() { alert('Привет!'); }
};
new User().sayHi();
В примере выше у класса нет имени, что один-в-один соответствует синтаксису функций. Но имя можно дать. Тогда оно, как и в Named Function Expression, будет доступно только внутри класса:
'use strict';
let SiteGuest = class User {
sayHi() { alert('Привет!'); }
};
new SiteGuest().sayHi(); // Привет
new User(); // ошибка
В примере выше имя User
будет доступно только внутри класса и может быть использовано, например, для создания новых объектов данного типа.
Наиболее очевидная область применения этой возможности – создание вспомогательного класса прямо при вызове функции.
Например, функция createModel
в примере ниже создаёт объект по классу и данным, добавляет ему _id
и пишет в «реестр» allModels
:
'use strict';
let allModels = {};
function createModel(Model, ...args) {
let model = new Model(...args);
model._id = Math.random().toString(36).slice(2);
allModels[model._id] = model;
return model;
}
let user = createModel(class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}, "Вася");
user.sayHi(); // Вася
alert( allModels[user._id].name ); // Вася
Геттеры, сеттеры и вычисляемые свойства
В классах, как и в обычных объектах, можно объявлять геттеры и сеттеры через get/set
, а также использовать […]
для свойств с вычисляемыми именами:
'use strict';
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// геттер
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// сеттер
set fullName(newValue) {
[this.firstName, this.lastName] = newValue.split(' ');
}
// вычисляемое название метода
["test".toUpperCase()]() {
alert("PASSED!");
}
};
let user = new User("Вася", "Пупков");
alert( user.fullName ); // Вася Пупков
user.fullName = "Иван Петров";
alert( user.fullName ); // Иван Петров
user.TEST(); // PASSED!
При чтении fullName
будет вызван метод get fullName()
, при присвоении – метод set fullName
с новым значением.
class
не позволяет задавать свойства-значенияВ синтаксисе классов, как мы видели выше, можно создавать методы. Они будут записаны в прототип, как например User.prototype.sayHi
.
Однако, нет возможности задать в прототипе обычное значение (не функцию), такое как User.prototype.key = "value"
.
Конечно, никто не мешает после объявления класса в прототип дописать подобные свойства, однако предполагается, что в прототипе должны быть только методы.
Если свойство-значение, всё же, необходимо, то можно создать геттер, который будет нужное значение возвращать.
Статические свойства
Класс, как и функция, является объектом. Статические свойства класса User
– это свойства непосредственно User
, то есть доступные из него «через точку».
Для их объявления используется ключевое слово static
.
Например:
'use strict';
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
static createGuest() {
return new User("Гость", "Сайта");
}
};
let user = User.createGuest();
alert( user.firstName ); // Гость
alert( User.createGuest ); // createGuest ... (функция)
Как правило, они используются для операций, не требующих наличия объекта, например – для фабричных, как в примере выше, то есть как альтернативные варианты конструктора. Или же, можно добавить метод User.compare
, который будет сравнивать двух пользователей для целей сортировки.
Также статическими удобно делать константы:
'use strict';
class Menu {
static get elemClass() {
return "menu"
}
}
alert( Menu.elemClass ); // menu
Наследование
Синтаксис:
class Child extends Parent {
...
}
Посмотрим как это выглядит на практике. В примере ниже объявлено два класса: Animal
и наследующий от него Rabbit
:
'use strict';
class Animal {
constructor(name) {
this.name = name;
}
walk() {
alert("I walk: " + this.name);
}
}
class Rabbit extends Animal {
walk() {
super.walk();
alert("...and jump!");
}
}
new Rabbit("Вася").walk();
// I walk: Вася
// and jump!
Как видим, в new Rabbit
доступны как свои методы, так и (через super
) методы родителя.
Это потому, что при наследовании через extends
формируется стандартная цепочка прототипов: методы Rabbit
находятся в Rabbit.prototype
, методы Animal
– в Animal.prototype
, и они связаны через __proto__
:
'use strict';
class Animal { }
class Rabbit extends Animal { }
alert( Rabbit.prototype.__proto__ == Animal.prototype ); // true
Как видно из примера выше, методы родителя (walk
) можно переопределить в наследнике. При этом для обращения к родительскому методу используют super.walk()
.
С конструктором – немного особая история.
Конструктор constructor
родителя наследуется автоматически. То есть, если в потомке не указан свой constructor
, то используется родительский. В примере выше Rabbit
, таким образом, использует constructor
от Animal
.
Если же у потомка свой constructor
, то, чтобы в нём вызвать конструктор родителя – используется синтаксис super()
с аргументами для родителя.
Например, вызовем конструктор Animal
в Rabbit
:
'use strict';
class Animal {
constructor(name) {
this.name = name;
}
walk() {
alert("I walk: " + this.name);
}
}
class Rabbit extends Animal {
constructor() {
// вызвать конструктор Animal с аргументом "Кроль"
super("Кроль"); // то же, что и Animal.call(this, "Кроль")
}
}
new Rabbit().walk(); // I walk: Кроль
Для такого вызова есть небольшие ограничения:
- Вызвать конструктор родителя можно только изнутри конструктора потомка. В частности,
super()
нельзя вызвать из произвольного метода. - В конструкторе потомка мы обязаны вызвать
super()
до обращения кthis
. До вызоваsuper
не существуетthis
, так как по спецификации в этом случае именноsuper
инициализируетthis
.
Второе ограничение выглядит несколько странно, поэтому проиллюстрируем его примером:
'use strict';
class Animal {
constructor(name) {
this.name = name;
}
}
class Rabbit extends Animal {
constructor() {
alert(this); // ошибка, this не определён!
// обязаны вызвать super() до обращения к this
super();
// а вот здесь уже можно использовать this
}
}
new Rabbit();
Итого
- Классы можно объявлять как в основном потоке кода, так и «инлайн», по аналогии с Function Declaration и Expression.
- В объявлении классов можно использовать методы, геттеры/сеттеры и вычислимые названия методов.
- При наследовании вызов конструктора родителя осуществляется через
super(...args)
, вызов родительских методов – черезsuper.method(...args)
.
Концепция классов, которая после долгих обсуждений получилась в стандарте ECMAScript, носит название «максимально минимальной». То есть, в неё вошли только те возможности, которые уж точно необходимы.
В частности, не вошли «приватные» и «защищённые» свойства. То есть, все свойства и методы класса технически доступны снаружи. Возможно, они появятся в будущих редакциях стандарта.