Typescript 学习之 - Class

353 阅读6分钟

类的概念

类包含了一类事物的抽象特点,包含这类事物的属性和方法。

类的实现

传统方法中我们通过构造函数实现类的概念,在ES6中我们终于迎来了 class,而 Typescript 除了实现ES6中的类的功能外,还添加了一些新的用法。

Typescript 中类的用法

属性和方法

使用 class 定义类,一个类可以包含以下几个成员

  • 构造函数 Constructor
  • 属性 Properties
  • 方法 Methods 通过 new 生成实例的时候,会自动调用 constructor
class Animal {
   public name;
   constructor(name) {
       this.name = name;
   }
   sayHi() {
       return `My name is ${this.name}`;
   }
}

let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

继承

使用关键字 extends 实现继承。

关于 super:

  • 在子类的 constructor 中,必须要使用 super(),他会调用父类的 constructor
  • 在子类中可以通过 super 来调用父类的方法。
class Animal {
   public name;
   constructor(name) {
       this.name = name;
   }
   sayHi() {
       return `My name is ${this.name}`;
   }
}

class Human extends Animal { 
    constructor(name) {
        super(name) // 调用父类的 constructor
    }
    sayMorning() {   
        console.log('moring')
        console.log(super.sayHi()) // 调用父类的方法
    }  
}
const man = new Human('Andy')
man.sayMorning()

上述示例中,子类 Human存在构造函数,必须要调用super,调用的同时会触发父类的构造函数,因次如果父类的构造函数存在参数,super的调用也需要传对应的参数。

在子类的方法 sayMorning 中,通过 super.sayHi() 调用了父类的 sayHi

修饰符

TypeScript 可以使用三种访问修饰符,分别是 publicprivateprotected,默认为 public

  • public 修饰的属性或方法是公有的,可以在任何地方访问到。
  • private 修饰的属性或方法是私有的
    • 不能在声明它的类的外部访问(实例和子类)
    • 当构造函数修饰为 private 时,该类不允许被继承或实例化
  • protected 修饰的属性或方法是受保护的
    • private 相似,但是可以在子类中访问。
    • 当构造函数被修饰为protected时,该类不允许被实例化,但可以被继承。

public

class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
console.log(a.name); // Tom

public 描述的属性可以在实例中访问

private

  1. 私有属性无法在实例中访问
class Employee {
    public name;
    private salary=10000;
    public constructor(name) {
        this.name = name;
    }
    printSalary() { 
       return this.salary // Employee类中访问私有属性 salary 是 OK 的
    }
}

let a = new Employee('Jack');

console.log(a.salary); 
// Property 'salary' is private and only accessible within class 'Employee'.
a.salary = 100000;
// Property 'salary' is private and only accessible within class 'Employee'.
  1. 私有属性无法在子类中访问
class Employee {
    public name;
    private salary=10000;
    public constructor(name) {
        this.name = name;
    }
    printSalary() { 
       return this.salary
    }
}
class Boy extends Employee{
    public constructor(name) {
        super(name)
        console.log(this.salary)
         //Property 'salary' is private and only accessible within class 'Employee'.
       
    }
}

上述示例中,Boy 继承了 Employee,在子类 Boy 中访问私有属性 salary会出现错误提示。

  1. constructor 被设置为 private 后无法创建实例
class Employee {
    private constructor(name) {
       
    }
}

let a = new Employee('Jack');
// Constructor of class 'Employee' is private and only accessible within the class declaration.

提示 Employee 的构造函数是私有的,只能在类中使用。

protected

  1. 受保护的属性可以在子类中访问,但是无法通过实例访问
class Employee {
    public name;
    protected salary=10000;
    public constructor(name) {
        this.name = name;
    }
    printSalary() { 
       return this.salary
    }
}
class Boy extends Employee{
    public constructor(name) {
        super(name)
        console.log(this.salary) // 受保护属性在子类中可以访问
    }
}
let a = new Employee('Jack');
console.log(a.salary)
// Property 'salary' is protected and only accessible within class 'Employee' and its subclasses.
  1. 构造函数被设置为受保护的属性后,无法创建实例
class Employee {
  public name;
  protected salary=10000;
  protected constructor(name) {
      this.name = name;
  }
  printSalary() { 
     return this.salary
  }
}

let a = new Employee('Jack');

// Constructor of class 'Employee' is protected and only accessible within the class declaration.

readonly修饰符

readonly 即一旦创建,不能修改。但不影响初始化,在类中声明或构造函数中初始化是允许的、也是有必要的。

class Employee {
    readonly empCode: number = 1; // Ok: declaration
    readonly empName: string;
    
    constructor(code: number, name: string)     {
        this.empName = name; //  Ok: initialized inside the class constructor
    }
}
let emp = new Employee(10, "John");
emp.empCode = 20; 
// Error:Cannot assign to 'empCode' because it is a constant or a read-only property.
emp.empName = 'Bill';
// Error:Cannot assign to 'empName' because it is a constant or a read-only property.

注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

class Animal {
  public readonly name
  public constructor(name) {
    this.name = name;
  }
}

参数属性

修饰符和 readonly 可以在构造函数参数中使用,等同于类中定义该属性的同时给该属性赋值,使代码更简洁

class Employee {
    constructor(protected name='human') {
    }
}

class Boy extends Employee{
    getName() {
        return this.name
    }
}
let boy = new Boy()
console.log(boy.getName()) // human

静态属性

属性前加上static关键字,就表示该属性不会被实例继承。

静态属性只能通过类来访问

class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42
const cat = new Animal()
console.log(cat.num)
// Property 'num' is a static member of type 'Animal'

存取器

使用 getter setter 可以改变属性的读取和赋值行为

可以防止我们意外的直接修改公有属性,并为我们检索和设置属性提供了更多控制。

class Person {
    private _name: string = '';
    get name(): string { 
        return  this._name
    }
    set name(newName) {
        alert(newName.length)
        if (newName && newName.length < 5) {
         throw new Error('Name is too short'); 
        } else {
         this._name=newName 
        }
       
    }
}
let person = new Person()
person.name = 'Jane Smith'
console.log(person.name)

上述示例中,_name 为私有属性,通过 getter 的 name 获取 _name 的值,在 setter 中我们限制的name的赋值长度需大于 5,小于 5 的话抛出一个异常。

另外 只带有 get不带有 set的存取器自动被推断为 readonly

抽象类

abstract 用于定义抽象类和其中的抽象方法

抽象类有以下几个特点

  • 抽象类不允许被实例化
  • 抽象类通过子类来实现
  • 抽象类方法和接口相似,只定义方法,不包含方法体
  • 抽象类方法必须通过子类实现

抽象类不允许被实例化: 实例化一个抽象类会出现异常提示

abstract class Animal{
    constructor(public name:string) { 
        this.name  = name
    }
    
}
const abstractAnimal = new Animal()
// Cannot create an instance of an abstract class.

抽象类通过子类来实现,子类继承了抽象类的属性和方法

class Cat extends Animal{
}

const cat = new Cat('miaomiao')
console.log(cat.name)

上述示例中,Cat 继承了抽象类 Animal,并且能够拥有 Animal 的属性name

抽象类方法和接口相似,只定义方法,不包含方法体,并且必须通过子类实现。

abstract class Animal{
    constructor(public name:string) { 
        this.name  = name
    }
    abstract getName(){}
    // Error:  Method 'getName' cannot have an implementation because it is marked abstract.
}
class Cat extends Animal{
// Error: Non-abstract class 'Cat' does not implement inherited abstract member 'getName' from class 'Animal'.
}

上述示例抛出了两个异常:

  • 抽象方法包含方法体后提示: getName为抽象类方法,不能被实现。
  • 继承类没有实现抽象方法提示: 非抽象类Cat没有实现抽象类Animal的方法getName

正确示例应为

abstract class Animal{
   constructor(public name:string) { 
       this.name  = name
   }
    abstract getName()
}

class Cat extends Animal{
   getName() {
       return this.name
    }
}
const cat = new Cat('miaomiao')
console.log(cat.getName()) // miaomiao

高级技巧

当我们声明一个类的时候同时创建了这个类的实例类型

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

let greeter: Greeter; // Greeter 的实例类型为Greeter
greeter = new Greeter("world");
console.log(greeter.greet());

上述示例表明:实例greeter的类型为 Greeter

官网的例子中有个细节需要好好理解一下

为何let greeterMaker: typeof Greeter = Greeter; 而不是 let greeterMaker: Greeter。如果将示例稍做更改为 let greeterMaker: Greeter 会出现如图所示的报错。

之所以报错是因为,let greeterMaker: Greeter 指的是实例的类型,而想获取静态元素其实需要的类型为构造函数Greeter,可以通过typeof Greeter来实现。

知识点总结:

  1. let greeterMaker: Greeter 指的是实例的类型
  2. let greeterMaker: typeof Greeter 指的是取 Greeter 类的类型,也就是构造函数的类型,这个类型中包含了类的所有静态成员和构造函数。

把类当做接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};