Классы
Немного о классах
Долгое время в JavaScript для создания однотипных объектов использовалась функция-конструктор. Функция-конструктор вызывается при помощи оператора new
. Эта функция заполняет полями и методами какой-то объект (обычно this
) и возвращает его (явно или не явно).
function Cat(name) {
this.name = name;
this.say = function() {
console.log(this.name, "say:", "meow");
}
}
const cat1 = new Cat("Tom");
const cat2 = new Cat("Lalaka");
cat1.say();
cat2.say();
В ES6 появился синтаксис, который позволяет объявлять классы. По сути класс - это функция-конструктор каких-то объектов с упрощенным синтаксисом наследования, объявления методов и еще некоторыми особенностями. Само понятие класса лучше отражает объектно-ориентированный подход разработки, чем использование функций-конструкторов и прототипов.
class Cat {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name, "say:", "meow");
}
}
const cat1 = new Cat("Tom");
const cat2 = new Cat("Lalaka");
cat1.say();
cat2.say();
Когда объявляется класс, в память кладется некоторый объект, который умеет создавать новые объекты при помощи оператора new
. Объект класса - это функция конструктор, в котором хранится логика по заполнению новых других объектов. Объекты, которые были созданы при помощи какого-то класса называются экзэмплярами или инстансами (instance) этого класса.
Важно научится четко различать, что такое "объект класса" и "инстанс класса". Выше сказано, что объект класса - это объект в памяти (это функция). Это означает, что с объектом класса можно работать как с обычным объектам: добавлять поля, присваивать в качестве значения переменной, передавать в параметры функции (и в параметры конструктора другого класса).
class Cat {
constructor(name) {
this.name = name;
}
}
const obj = Cat;
const cat1 = new obj("Tom");
function create(classObj) {
return new classObj("Tom");
}
const cat2 = create(Cat);
Cat.foo = function() {
console.log("Функция у объекта класса");
}
Cat.foo();
Инстансы класса - это самостоятельные объекты, которые были сконструированы этого класса. Они не зависят друг от друга (если это не сделано специально) и с классом их связывает только то, что он являлся их создателем.
Объявление классов в TypeScript имеет ряд особенностей по сравнению с объявлением классов в JavaScript.
Поля классов
Класс может иметь поля. Для того, чтобы объявить поле, нужно указать его название и тип.
class A {
b: number;
c: string;
e: { a: number; b: string };
}
Поля могут иметь модификатор доступа. В TypeScript существует 3 модификатора доступа:
public
- поле является публичным. Читать и изменять значение этого поля можно в любом местеprivate
- поле является приватным. Читать и изменять значение этого поля можно только из методов самого классаprotected
- поле является защищенным. Чистать и изменять значение этого поля можно из методов самого класса и его наследников
class A {
public a: number;
private b: string;
protected c: boolean;
}
const a = new A();
a.a = 10;
a.b = "lalaka"; //Error: Property 'b' is private and only accessible within class 'A'.
a.c = true; //Error: Property 'c' is protected and only accessible within class 'A' and its subclasses.
const q1 = a.a;
const q2 = a.b; //Error: Property 'b' is private and only accessible within class 'A'.
const q3 = a.c; //Error: Property 'c' is protected and only accessible within class 'A' and its subclasses.
Модификаторы доступа полей нужны для того, чтобы правильно инкапсулировать логику внутри класса. private
и protected
поля хранят внутренне состояние класса. Это позволяет классу гарантировать, что значения в этих полях будут соответствовать внутренней логике класса (или потомка). Важно, что защита полей присутствует только на этапе компиляции. В JavaScript нет (пока) модификаторов доступа полей класса, поэтому в runtime значения приватных полей может быть изменено откуда угодно.
Если у поля не задан никакой модификатор доступа, то это поле public
по умолчанию.
Для того, чтобы поле было доступно только на чтение, в TypeScript существует модификатор поля readonly
. Он может применятся в месте с модификатором доступа. readonly
поле должно быть инициализировано либо во время объявления, либо в конструкторе (об этом позже).
class A {
public readonly a: number = 10;
private readonly b: string;
constructor() {
this.b = "lalaka";
}
}
const a = new A();
const q1 = a.a;
a.a = q1; //Error: Cannot assign to 'a' because it is a read-only property.
Поле класса может быть статичным. Статичное поле - это поле, значение которого хранится не в каждом конкретном инстансе класса (у каждого свое), а в самом объекте класса. Статичное поле помечается модификатором static
. К статичному полю можно применять модификаторы доступа и модификатор readonly
.
class A {
static readonly a: { p: string } = { p: "lalaka" };
private static readonly c: string = "lalaka";
}
const q1 = A.a; //q1 = { p: "lalaka" }
A.a.p = "malaka"; //A.a = { p: "malaka" }
q1.p; //q1 = { p: "malaka" }
Важно отметить, что к статичному полю через инстанс класса доступа нет.
new A().a //Error: Property 'a' is a static member of type 'A'
Из-за того, что статичные поля присваиваются только объекту самого класса, а не статичные - только инстансу класса, в TypeScript можно объявить у одного и того же класс статичное и не статичное поля с одинаковым именем:
class A {
static a: number = 10;
a: boolean = true;
}
A.a // 10
new A().a // true
Поля класса могут быть необязательными. Для этого после названия параметра нужно указать символ ?
(как у объектов).
Конструктор
Выше уже было показано, как объявляется конструктор класса. Конструктор класса - это функция, которая вызывается для создания нового инстанса класса. Конструктор объявляется при помощи ключевого слова constructor
. В конструкторе можно изменять значения полей класса (как публичных, так и приватных).
class A {
private a: number;
public b: string;
constructor() {
this.a = 10;
this.b = "lalaka";
}
}
Ключевое слово this
ссылается на тот инстанс класса, который сейчас конструируется. Этот же инстанс класса вернется в результате команды new A()
. Конструктор класса - это функция. Она может принимать параметры. Параметры конструктора типизируются также, как типизируются параметры обычной функции.
class A {
private a: number;
private b: string;
private c?: boolean;
constructor(a: number, b = "lalaka", c?: boolean) {
this.a = a;
this.b = b;
this.c = c;
}
}
const a1 = new A(10);
const a2 = new A(15, "malaka");
const a3 = new A(20, "balaka", false);
Выше также было указано, что поля класса можно инициализировать во время объявления (in-place).
class A {
private a: number = 10;
}
При создании инстанса класса, in-place поля инициализируются до вызова конструктора. К их значениям можно получить доступ из конструктора. Также в конструкторе можно изменить in-place значение поля.
class A {
private a: number = 10;
public b: number = 100;
constructor() {
this.b = this.a
}
}
new A().b // 10
Для удобства работы в TypeScript есть возможность объявлять поля сразу в конструкторе класса. Если параметру конструктора указать модификатор доступа, то у класс появится поле с именем и значением параметра и указанным модификатором доступа:
class A {
constructor(private a: number, public b: string) {
}
}
const q1 = new A(10, "lalaka");
q1.b // "lalaka"
Методы
Помимо полей, класс может иметь методы. Метод класс - это функция, которая существует в рамках инстанса класса (статичная в рамках объекта класса). Эта функция имеет доступ к внутреннему состоянию класса. Она, как и другие функции, может иметь или не иметь возвращаемое значение и параметры класса (объявлять generic-параметры).
class A {
private q: number = 2;
multiply(a: number): number {
return this.q*a;
}
}
const a = new A();
const q1 = a.multiply(15); // 30
const q2 = a.multiply(10); // 20
Методы класса могут иметь модификаторы доступа. Они такие же, как и модификаторы доступа полей. public
методы могут быть вызваны, как внутри класса, так и снаружи; private
- только внутри класса; protected
- внутри класса или внутри класса наследника.
class A {
public say() {
console.log(this.getWord("Hello"));
}
private getWord(word: string): string {
return word;
}
}
const a = new A();
a.say();
a.getWord("lalaka"); //Error: Property 'getWord' is private and only accessible within class 'A'.
Модификатор readonly
к методам не применим. А вот модификатор static
применим и работает похожим образом, как и для полей. Метод присваивается не инстансам класса, а самому объекту класса. Соответственно, статичный метод не имеет доступа к полям инстансов класса, но имеет доступ к статичным полям и другим статичным методам.
class A {
private static word: string = "Hello";
public static say() {
console.log(this.getWord())
}
private static getWord(): string {
return this.word;
}
}
A.say();
Generic-параметры
Классу можно указывать generic-параметры. Их нужно указывать в <>
сразу после имени класса. Generic-и выводятся по тем же принципам, что и при вызовах функций.
class A<T> {
public readonly a: T
constructor(a: T) {
this.a = a;
}
}
const a1 = new A(10);
const a2 = new A("lalaka");
const a3 = new A<true>(true);
Методы класса могут объявлять собственные generic-и и использовать generic-и объявленные для класса.
class A<T> {
public readonly a: T
constructor(a: T) {
this.a = a;
}
print<R>(b: R) {
console.log(this.a, b);
}
}
const a = new A(10);
a.print("lalaka");
a.print<true>(true);
Наследование
В TypeScript поддерживается возможность наследования классов. Наследование - одна из ключевых механик объектно-ориентированного программирования. При помощи наследования можно передать (унаследовать) поведение одного класса другому классу.
class Figure {
print() {
console.log("I'm a figure");
}
}
class Circle extends Figure {
radius: number = 15;
}
const q1 = new Circle();
q1.radius; // 15
q1.print();
В примере выше класс Figure
содержит метод print
. Класс Circle
наследуется (расширяет) класс Figure
. В этом случаи Circle
является наследником класса Figure
, а класс Figure
- базовом классом (или родительским классом, или суперклассом (superclass)) для Circle
. Все поведение, которое определяется в классе Figure
(в нашем случаи это метод print
) передается классу Circle
. При этом сам класс Circle
объявляет свое собственное специфичное поведение (поле radius
) в дополнение к унаследованному. Поэтому инстанс класса Circle
имеет и поле radius
и метод print
.
Если у базового класса объявлен конструктор с обязательными параметрами, то его наследник в своем конструкторе должен вызывать конструктор базового класса. Иначе будет ошибка компиляции.
class Rect {
constructor(public width: number, public height: number) {
}
}
class Square extends Rect {
constructor(side: number) {
super(side, side);
}
}
const rect = new Rect(15, 10);
const square = new Square(5);
Ранее были рассмотрены модификаторы доступа полей и методов. Для того, чтобы сделать какое-то поле или метод доступным только внутри класса или внутри наследников этого класса, используется модификатор protected
. private
поля и методы базового класса доступны только внутри самого базового класса и наследникам не передаются.
class Rect {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
protected calcArea(): number {
return this.width*this.height;
}
}
class Square extends Rect {
constructor(side: number) {
super(side, side);
}
printArea() {
const q = this.width; //Error: Property 'width' is private and only accessible within class 'Rect'.
console.log(this.calcArea());
}
}
const square = new Square(5);
square.printArea();
square.calcArea(); //Error: Property 'calcArea' is protected and only accessible within class 'Rect' and its subclasses.
Так как наследник расширяет базовый класс - он является надмножеством над базовым классом. С точки зрения типов это означает, что инстансы наследников базового класса могут передаваться туда, где требуется значение с типом базового класса.
class Rect {
constructor(public width: number, public height: number) {
}
}
class Square extends Rect {
constructor(side: number) {
super(side, side);
}
onlySquareMethod() {
}
}
const q1: Square = new Square(10);
const q2: Rect = q1;
q1.onlySquareMethod();
q2.onlySquareMethod(); //Error: Property 'onlySquareMethod' does not exist on type 'Rect'.
В коде выше переменная q2
имеет тип Rect
, но ей присваивается значение q1: Square
. Это возможно, потому что тип Square
имеет все поля типа Rect
. Однако, не смотря на то, что в q2
лежит инстанс класса Square
, поля и методы этого типа использовать нельзя, потому что тип переменной q2: Rect
и TypeScript в общем случаи не может гарантировать, что в значении этой переменной лежит Square
.
Наследоваться сразу от двух классов нельзя. TypeScript (как JavaScript) не поддерживает множественное наследование. Можно наследовать один класс от второго, а второй от третьего.
В TypeScript класс может быть абстрактным. Абстрактный класс - это класс, от которого можно наследовать другие классы, но нельзя создавать инстансы этого класса. Абстрактный класс помечается ключевым словом abstract
.
abstract class Figure {
print() {
console.log("I'm a figure");
}
}
class Circle extends Figure {
}
const circle = new Circle();
const figure = new Figure(); //Error: Cannot create an instance of an abstract class.
Абстрактные классы могут иметь абстрактные методы. Абстрактные методы - это методы, у которых объявлена сигнатура, но не объявлено тело метода. Абстрактный метод помечается модификатором abstract
. Наследник абстрактного класса обязан объявить у себя метод с такой же сигнатурой и определить реализацию этого метода. Если наследник не реализует абстрактные методы - будет ошибка компиляции.
abstract class A {
abstract show(p: number): string;
}
//Error: Non-abstract class 'B1' does not implement inherited abstract member 'show' from class 'A'.
class B1 extends A {
}
Если наследником класса с абстрактными методами тоже является абстрактный класс, то абстрактные методы реализовывать не обязательно.
abstract class A {
abstract show(p: number): string;
}
abstract class B2 extends A {
}
Абстрактные методы стоит воспринимать как интерфейс. Они гарантируют, что независимо от реализации абстрактного класса, они будут реализованы и иметь фиксированную сигнатуру.
abstract class BasePrinter {
abstract print(n: number): void;
}
class ConsolePrinter extends BasePrinter {
print(n: number): void {
console.log(n);
}
}
class DomPrinter extends BasePrinter {
print(n: number): void {
const element = document.querySelector("#print");
if (element != null) {
element.textContent = n.toString();
}
}
}
const printers: BasePrinter[] = [new ConsolePrinter(), new DomPrinter()];
printers.forEach(p => p.print(10));
Класс, как интерфейс
Инстанс класса - это объект. Как и любой другой объект в TypeScript, он имеет тип. Интерфейсом взаимодействия с инстансом класса из внешнего мира являются его публичные поля и методы. Поэтому, если тип объекта требует наличие каких-либо полей или методов у объекта, и тип класса имеет все эти поля и методы, то инстансы этого класса совместимы с типом объекта.
interface A {
x: number;
}
class B {
constructor(public x: number) {
}
}
class C {
constructor(public x: string) {
}
}
const a1: A = new B(10);
const a2: A = new C("10"); /* Error:
Type 'C' is not assignable to type 'A'.
Types of property 'x' are incompatible.
Type 'string' is not assignable to type 'number'.
*/
Тип класса сам может выступать в роли интерфейса.
class A {
public x: number = 15;
}
const a: A = { x: 40 };
В коде выше переменная a
типизирована, как A
. При этом A
- это класс. Конструкция a: A
не требует присвоения только инстансов класса A
. Это конструкция требует любое совместимое с интерфейсом класса значение.
Для более строгой типизации TypeScript
поддерживает реализацию интерфейса классом. Для этого используется ключевое слово implements
.
interface Figure {
print: () => void;
}
class Circle implements Figure {
print() {
console.log("I'm a circle");
}
}
Если указано, что класс реализует какой-то интерфейс, но в классе (или его базовом классе) не объявлены какие-то поля или методы, указанные в интерфейсе, то TypeScript выдаст ошибку компиляцию
interface Figure {
print: () => void;
}
class Circle implements Figure {
} /* Error:
Class 'Circle' incorrectly implements interface 'Figure'.
Property 'print' is missing in type 'Circle' but required in type 'Figure'.
*/
Класс может реализовывать сразу несколько интерфейсов - их нужно перечислять через запятую после ключевого слова implements
. Класс может одновременно наследоваться от другого класса и реализовывать множество интерфейсов.
interface A {
a: number;
}
interface B {
b: string;
}
class X {
public a: number = 300;
}
class Y extends X implements A, B {
constructor(public b: string) {
super();
}
}
Как было сказано ранее, классу Y
не обязательно реализовывать интерфейсы A
и B
, для того, чтобы типы его инстансов были совместимы с этими интерфейсами. Реализация интерфейсов во многом нужна для более строгой типизации и облегчения работы в IDE.
Объект класса, как тип
Ранее уже говорилось, что объект класса - это своего рода функция, которая конструирует другие объекты (инстансы класса). Так как это объект, соответственно у него есть какой-то тип.
Объект класса можно вызывать как функцию с определенными параметрами, которая вернет определенное значение. Но есть одна важная особенность. Эту функцию-конструктор можно вызывать только с ключевым словом new
. Поэтому тип объекта класса можно описать при помощи callable со следующей модификацией:
interface ClassType {
new (a: number, b: string): {a: number, b: string, c: boolean}
}
Выше был объявлен тип, который описывает множество объектов, которые можно вызывать с оператором new
и двумя параметрами с заданными типами, и в результате этого вызова вернется объект с заданным интерфейсом.
interface ClassType {
new (a: number, b: string): {a: number, b: string, c: boolean}
}
class A {
c: boolean = false;
constructor(public a: number, public b: string) {
}
}
class B {
constructor(public a: number, public b: string) {
}
}
const classType1: ClassType = A;
const classType2: ClassType = B; /* Error:
Type 'typeof B' is not assignable to type 'ClassType'.
Property 'c' is missing in type 'B' but required in type '{ a: number; b: string; c: boolean; }'.
*/
Например, можно объявить тип-помощник, который будет возвращать тип объекта класса.
type TypeOf<T> = { new (...args: any[]): T }
class A {
}
class B {
constructor(public b: string) {
}
}
const classA: TypeOf<A> = A;
const classB: TypeOf<B> = B;
const wrong: TypeOf<A> = new A(); /* Error:
Type 'A' is not assignable to type 'TypeOf<A>'.
Type 'A' provides no match for the signature 'new (...args: any[]): A'.
*/
Задания
Last updated
Was this helpful?