Spec-Zone.ru › TypeScript 5.1

Классы

Эта страница руководства заменена, перейти к новой странице

Традиционный JavaScript использует функции и наследование на основе прототипов для построения многократно используемых компонентов, но это может показаться немного неудобным для программистов, более привыкших к объектно-ориентированному подходу, где классы наследуют функциональность, а объекты создаются из этих классов. Начиная с ECMAScript 2015, также известного как ECMAScript 6, программисты JavaScript могут создавать свои приложения, используя этот объектно-ориентированный подход на основе классов. В TypeScript мы позволяем разработчикам использовать эти методы сейчас и компилировать их в JavaScript, который работает во всех основных браузерах и платформах, не дожидаясь следующей версии JavaScript.

Классы

Давайте рассмотрим простой пример на основе классов:

class Greeter {
  greeting: string;
 
  constructor(message: string) {
    this.greeting = message;
  }
 
  greet() {
    return "Hello, " + this.greeting;
  }
}
 
let greeter = new Greeter("world");

Синтаксис должен быть знаком, если вы использовали C# или Java раньше. Мы объявляем новый класс Greeter. Этот класс имеет три члена: свойство, называемое greeting, конструктор и метод greet.

Вы заметите, что в классе, когда мы ссылаемся на один из членов класса, мы добавляем префикс this.. Это обозначает, что это доступ к члену.

В последней строке мы создаём экземпляр класса Greeter с помощью new. Это вызывает конструктор, который мы определили ранее, создавая новый объект с формой Greeter и выполняя конструктор для его инициализации.

Наследование

В TypeScript мы можем использовать стандартные объектно-ориентированные шаблоны. Один из самых фундаментальных шаблонов в программировании на основе классов — это возможность расширять существующие классы для создания новых с использованием наследования.

Давайте рассмотрим пример:

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}
 
class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}
 
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

Этот пример демонстрирует самую простую функцию наследования: классы наследуют свойства и методы из базовых классов. Здесь Dog — это производный класс, который наследуется от Animal базового класса с использованием ключевого слова extends. Производные классы часто называются подклассами, а базовые классы — суперклассами.

Поскольку Dog расширяет функциональность от Animal, мы смогли создать экземпляр Dog, который мог как bark(), так и move().

Теперь давайте рассмотрим более сложный пример.

class Animal {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}
 
class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}
 
class Horse extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters);
  }
}
 
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
 
sam.move();
tom.move(34);

Этот пример охватывает несколько других функций, которые мы не упоминали ранее. Опять же, мы видим ключевые слова extends используемые для создания двух новых подклассов Animal: Horse и Snake.

Одно отличие от предыдущего примера состоит в том, что каждый производный класс, содержащий функцию конструктора, обязательно должен вызывать super(), что выполнит конструктор базового класса. Более того, прежде чем мы когда-либо обратимся к свойству this в теле конструктора, мы обязательно должны вызвать super(). Это важное правило, которое TypeScript будет контролировать.

Пример также показывает, как переопределять методы в базовом классе методами, специализированными для подкласса. Здесь как Snake, так и Horse создают метод move, который переопределяет метод move из Animal, предоставляя ему функциональность, специфичную для каждого класса. Обратите внимание, что даже если tom объявлен как Animal, так как его значение — Horse, вызов tom.move(34) вызовет метод переопределения в Horse:

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

Модификаторы public, private и protected

По умолчанию public

В наших примерах мы могли свободно обращаться к членам, объявленным в наших программах. Если вы знакомы с классами на других языках, вы могли заметить, что в вышеприведенных примерах нам не пришлось использовать слово public для этого; например, C# требует, чтобы каждый член был явно помечен public для видимости. В TypeScript каждый член public по умолчанию.

Вы всё равно можете явно отметить член public. Мы могли бы написать класс Animal из предыдущего раздела следующим образом:

class Animal {
  public name: string;
 
  public constructor(theName: string) {
    this.name = theName;
  }
 
  public move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

ECMAScript Приватные поля

С TypeScript 3.8, TypeScript поддерживает новый синтаксис JavaScript для приватных полей:

class Animal {
  #name: string;
  constructor(theName: string) {
    this.#name = theName;
  }
}
 
new Animal("Cat").#name;

Этот синтаксис встроен в среду выполнения JavaScript и может иметь лучшие гарантии изоляции каждого приватного поля. В настоящее время лучшей документацией для этих приватных полей являются примечания к релизу TypeScript 3.8 в блоге разработчиков.

Понимание приватных членов TypeScript

TypeScript также имеет свой собственный способ объявить член, помеченный как private, его нельзя использовать вне своего содержащего класса. Например:

class Animal {
  private name: string;
 
  constructor(theName: string) {
    this.name = theName;
  }
}
 
new Animal("Cat").name;

TypeScript — это структурная система типов. При сравнении двух различных типов, независимо от их происхождения, если типы всех членов совместимы, то сами типы совместимы.

Однако при сравнении типов, имеющих private и protected члены, мы рассматриваем эти типы по-разному. Для того чтобы два типа считались совместимыми, если один из них имеет private член, то другой должен иметь private член, возникший в том же объявлении. То же самое относится к protected членам.

Давайте посмотрим на пример, чтобы лучше понять, как это работает на практике:

class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}
 
class Rhino extends Animal {
  constructor() {
    super("Rhino");
  }
}
 
class Employee {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}
 
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
 
animal = rhino;
animal = employee;

В этом примере у нас есть Animal и Rhino, где Rhino — подкласс Animal. У нас также есть новый класс Employee, который выглядит идентично Animal по форме. Мы создаём некоторые экземпляры этих классов и затем пытаемся назначать их друг другу, чтобы посмотреть, что произойдёт. Поскольку Animal и Rhino совместно используют private сторону своей формы из одного объявления private name: string в Animal, они совместимы. Однако это не относится к Employee. Когда мы пытаемся назначить от Employee к Animal, мы получаем ошибку, что эти типы не совместимы. Хотя Employee также имеет private член, называемый name, это не тот, который мы объявили в Animal.

Понимание protected

Модификатор protected действует очень похоже на модификатор private за исключением того, что члены, объявленные protected также могут быть доступны в производных классах. Например,

class Person {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}
 
class Employee extends Person {
  private department: string;
 
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }
 
  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}
 
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name);

Обратите внимание, что хотя мы не можем использовать name снаружи Person, мы всё ещё можем использовать его внутри метода экземпляра Employee , поскольку Employee наследуется от Person.

Конструктор также может быть помечен protected. Это означает, что класс не может быть создан вне содержащего его класса, но может быть расширен. Например,

class Person {
  protected name: string;
  protected constructor(theName: string) {
    this.name = theName;
  }
}
 
// Employee can extend Person
class Employee extends Person {
  private department: string;
 
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }
 
  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}
 
let howard = new Employee("Howard", "Sales");
let john = new Person("John");

Модификатор readonly

Вы можете сделать свойства только для чтения, используя ключевое слово readonly. Свойства только для чтения должны быть инициализированы при их объявлении или в конструкторе.

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
 
  constructor(theName: string) {
    this.name = theName;
  }
}
 
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit";

Параметрические свойства

В нашем последнем примере нам пришлось объявить свойство только для чтения name и параметр конструктора theName в классе Octopus. Это необходимо для того, чтобы значение theName было доступно после выполнения конструктора Octopus . Параметрические свойства позволяют создавать и инициализировать член в одном месте. Вот переработанный предыдущий класс Octopus с использованием параметрического свойства:

class Octopus {
  readonly numberOfLegs: number = 8;
  constructor(readonly name: string) {}
}
 
let dad = new Octopus("Man with the 8 strong legs");
dad.name;

Обратите внимание, как мы полностью убрали theName и просто используем сокращённый параметр readonly name: string в конструкторе для создания и инициализации члена name. Мы объединили объявления и присваивания в одном месте.

Параметрические свойства объявляются путём добавления модификатора доступа или readonly к параметру конструктора, или обоих. Использование private для параметрического свойства объявляет и инициализирует приватный член; аналогично делается для public, protected, и readonly.

Обработчики доступа

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

Давайте преобразуем простой класс, используя геттеры и сеттеры. Сначала рассмотрим пример без геттеров и сеттеров.

class Employee {
  fullName: string;
}
 
let employee = new Employee();
employee.fullName = "Bob Smith";
 
if (employee.fullName) {
  console.log(employee.fullName);
}

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

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

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

const fullNameMaxLength = 10;
 
class Employee {
  private _fullName: string = "";
 
  get fullName(): string {
    return this._fullName;
  }
 
  set fullName(newName: string) {
    if (newName && newName.length > fullNameMaxLength) {
      throw new Error("fullName has a max length of " + fullNameMaxLength);
    }
 
    this._fullName = newName;
  }
}
 
let employee = new Employee();
employee.fullName = "Bob Smith";
 
if (employee.fullName) {
  console.log(employee.fullName);
}

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

Несколько замечаний об обработчиках доступа:

Во-первых, обработчики доступа требуют, чтобы компилятор выводил ECMAScript 5 или выше. Низведение до ECMAScript 3 не поддерживается. Во-вторых, обработчики доступа с get и без set автоматически предполагаются readonly. Это полезно при генерации файла .d.ts из вашего кода, потому что пользователи вашего свойства могут видеть, что они не могут его изменить.

Статические свойства

До этого момента мы говорили только о членах экземпляра класса, тех, которые появляются на объекте при его создании. Мы также можем создавать статические члены класса, которые видны на самом классе, а не на экземплярах. В этом примере мы используем static для начала, так как это общее значение для всех таблиц. Каждый экземпляр получает доступ к этому значению, добавляя имя класса. Аналогично добавлению this. перед доступом к экземплярам, здесь мы добавляем Grid. перед доступом к статическим членам.

class Grid {
  static origin = { x: 0, y: 0 };
 
  calculateDistanceFromOrigin(point: { x: number; y: number }) {
    let xDist = point.x - Grid.origin.x;
    let yDist = point.y - Grid.origin.y;
    return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
  }
 
  constructor(public scale: number) {}
}
 
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
 
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));

Абстрактные классы

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

abstract class Animal {
  abstract makeSound(): void;
 
  move(): void {
    console.log("roaming the earth...");
  }
}

Методы внутри абстрактного класса, помеченные как абстрактные, не содержат реализации и должны быть реализованы в производных классах. Абстрактные методы имеют синтаксис, похожий на методы интерфейса. Оба определяют сигнатуру метода без тела метода. Однако абстрактные методы должны включать ключевое слово abstract и могут необязательно включать модификаторы доступа.

abstract class Department {
  constructor(public name: string) {}
 
  printName(): void {
    console.log("Department name: " + this.name);
  }
 
  abstract printMeeting(): void; // must be implemented in derived classes
}
 
class AccountingDepartment extends Department {
  constructor() {
    super("Accounting and Auditing"); // constructors in derived classes must call super()
  }
 
  printMeeting(): void {
    console.log("The Accounting Department meets each Monday at 10am.");
  }
 
  generateReports(): void {
    console.log("Generating accounting reports...");
  }
}
 
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: department is not of type AccountingDepartment, cannot access generateReports

Расширенные приемы

Функции-конструкторы

Когда вы объявляете класс в TypeScript, вы на самом деле создаете несколько объявлений одновременно. Первое — это тип экземпляра класса.

class Greeter {
  greeting: string;
 
  constructor(message: string) {
    this.greeting = message;
  }
 
  greet() {
    return "Hello, " + this.greeting;
  }
}
 
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"

Здесь, когда мы говорим let greeter: Greeter, мы используем Greeter как тип экземпляров класса Greeter. Для программистов, знакомых с другими объектно-ориентированными языками, это почти привычно.

Мы также создаем другую величину, которую мы называем функцией-конструктором. Это функция, которая вызывается при создании экземпляров класса.

new

Чтобы увидеть, как это выглядит на практике, давайте посмотрим на JavaScript, созданный по вышеупомянутому примеру:

let Greeter = (function () {
  function Greeter(message) {
    this.greeting = message;
  }
 
  Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
  };
 
  return Greeter;
})();
 
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"

Здесь let Greeter будет назначена функция-конструктор. Когда мы вызываем new и запускаем эту функцию, мы получаем экземпляр класса. Функция-конструктор также содержит все статические члены класса. Другой способ взглянуть на каждый класс — это то, что у него есть сторона экземпляра и сторона статических членов.

Давайте немного изменим пример, чтобы показать эту разницу:

class Greeter {
  static standardGreeting = "Hello, there";
  greeting: string;
  greet() {
    if (this.greeting) {
      return "Hello, " + this.greeting;
    } else {
      return Greeter.standardGreeting;
    }
  }
}
 
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet()); // "Hello, there"
 
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
 
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // "Hey there!"
 
let greeter3: Greeter;
greeter3 = new Greeter();
console.log(greeter3.greet()); // "Hey there!"

В этом примере greeter1 работает аналогично предыдущему. Мы создаем экземпляр класса Greeter, и используем этот объект. Это мы уже видели раньше.

Затем мы используем класс напрямую. Здесь мы создаем новую переменную с именем greeterMaker. Эта переменная будет содержать сам класс, или, другими словами, его функцию-конструктор. Здесь мы используем Greeter, то есть «дайте мне тип самого класса Greeter», а не тип экземпляра. Или, точнее, «дайте мне тип символа под названием Greeter», который является типом функции-конструктора. Этот тип будет содержать все статические члены Greeter вместе с конструктором, который создает экземпляры класса Greeter . Мы показываем это, используя new на greeterMaker, создавая новые экземпляры Greeter и вызывая их, как и раньше. Стоит также упомянуть, что изменение статических свойств не рекомендуется; здесь greeter3 имеет "Hey there!" вместо "Hello, there" в standardGreeting.

Использование класса в качестве интерфейса

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

class Point {
  x: number;
  y: number;
}
 
interface Point3d extends Point {
  z: number;
}
 
let point3d: Point3d = { x: 1, y: 2, z: 3 };

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/classes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API