点亮TypeScript技能树(二)

1,000 阅读5分钟

1. 类

1.1 构造器

  1. 使用类的例子
class Dog {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  // 引用任何一个类成员的时候都用了this.它表示我们访问的是类的成员
  say() {
    return console.log(`${this.name}今年${this.age}岁`);
  }
}
const erha = new Dog("erha", 4);
erha.say();
  1. 分析
    1. 我们声明了一个Dog类,这个类有4个成员:两个属性(name,age),一个构造函数和一个say方法
    2. 使用new构造了Dog类的一个实例。它会调用之前定义的构造函数,创建一个Dog类型的新对象,并执行构造函数初始化它。
    3. 在引用类的成员的时候都用了this。它表示我们访问的是类的成员。
    4. 在实例方法中,this就表示当前的实例。

1.2 继承

  1. 在TypeScript里,使用继承来扩展现有的类。
  2. 继承案例代码(一)
// 在TypeScript里,允许使用继承来扩展现有的类
class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal move ${distanceInMeters}m`);
  }
}
class Cat extends Animal {
  bark() {
    console.log("Woof Woof");
  }
}

const cat = new Cat();
cat.bark();
cat.move();
cat.move(20);
  • 输出结果
Woof Woof
Animal move 0m
Animal move 20m
  1. 分析
    1. 类从基类中继承了属性和方法。Cat是一个派生类,它派生自Animal基类,通过extends关键字。
    2. 派生类通常被称为子类,基类通常被称为超类
    3. 因为Cat继承了Animal的功能,因此我们可以创建一个Cat的实例,它能够bark()和move()
  2. 继承案例代码(二)
class Animals {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} move ${distanceInMeters}`);
  }
}
// 继承Animals类
class Snake extends Animals {
  constructor(name: string) {
    super(name);
  }
  move(distanceINmeters = 5) {
    console.log("Slithering...");
    super.move(distanceINmeters);
  }
}
// 继承Animals类
class Horse extends Animals {
  constructor(name: string) {
    super(name);
  }
  move(distanceINmeters = 45) {
    console.log("Galloping...");
    super.move(distanceINmeters);
  }
}

let sam = new Snake("Sammy the Python");
let tom = new Horse("Tommy the Palomino");
sam.move();
tom.move();
  • 代码执行结果
Slithering...
Sammy the Python move 5
Galloping...
Tommy the Palomino move 45
  1. 分析
    1. 使用extends关键字创建了Animals的两个子类: Snake和Horse
    2. 两个派生类包含了一个构造函数,它必须调用super(),它会执行基类的构造函数。
    3. 在构造函数里访问this的属性之前,我们一定要调用super()

1.3 修饰符

  1. public
    1. 在TypeScript里,成员都默认为public。也可以明确的将一个成员 标记成public.
  2. private
    1. 当成员被标记成private时,它就不能在声明它的类的外部访问。
    2. 只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的
  3. protected
    1. protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。
    2. 构造函数也可以被标记成protected。这意味着这个类不能在包含它的类外被实例化,但是能被继承。
  4. 使用protected案例代码
class Person2 {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Employee2 extends Person2 {
  private department: string;
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }
  public getElveatorPitch() {
    return `name is ${this.name},department is ${this.department}`;
  }
}
let Bob = new Employee2("Bob", "sales");
Bob.getElveatorPitch();
console.log(Bob.name); // 错误
2. 代码分析
   Person2类有protected属性name, 派生类Employee2实例方法可以访问到Person2类的name属性,
   但是不能在Person2类外部使用name属性
  

4. readonly修饰符 1. 使用readonly关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。
5. 静态属性 static\

1.4 抽象类 abstract

1. 抽象类作为其他派生类的基类使用。它们一般不会直接被实例化。`abstract`关键字是用于定义抽象类和在抽象类内部定义抽象方法。
2. 抽象类中的抽象方法不包含具体实现,并且必须在派生类中实现。
3. 抽象方法必须包含`abstract`关键字。   
  1. 案例代码
abstract class Animal3 {
  abstract makeSound(): void;
  move(): void {
    console.log("rooming the earch...");
  }
}
abstract class Department {
  constructor(public name: string) {}
  printName(): void {
    console.log("Department name:" + this.name);
  }
  // 抽象类中抽象方法不包含具体实现并且必须在派生类中实现
  abstract printMeeting(): void;
}
class AccountingDepartment extends Department {
  // 在派生类的构造函数中调用super()
  constructor() {
    super("Accounting and Auditing...");
  }
  printMeeting(): void {
    console.log(" The Accounting Department meets each Monday at 10am..");
  }
  generateReports(): void {
    console.log("generating accounting reports...");
  }
}
// 创建一个抽象类类型的引用
let department: Department;
department = new Department(); // 会出错,不能创建一个抽象类的实例
department = new AccountingDepartment(); 
department.printName();
department.printMeeting();
department.generateReports(); // 方法在声明抽象类中不存在
  • 代码分析
    1. 声明了两个抽象类Animal3和Department,
    1. Department抽象类中有,构造器、printName方法、抽象方法printMeeting(抽象类中的定义抽象方法只定义方法签名,不包含方法体,但是,在派生类中必须要实现抽象类定义的抽象方法)
    1. AccountingDepartment类实现了抽象类Department,在构造函数中调用Super(),定义自身的方法generateReports,实现继承抽象类中的抽象方法。
    1. 不能直接创建一个抽象类的实例对象
    1. 允许对一个抽象子类进行实例化和赋值

2. 接口

2.1 使用接口

  • TypeScript的核心原则之一是对值所具有的结构进行检查。
  • 接口用来定义一个类的结构,用来定义一个类中应该包含那些属性和方法
  • 可以重复定义接口
  • 接口中的所有的属性都不能有实际值
  • 接口只定义对象的结构,而不考虑实际值
  • 接口中的方法,只是定义方法签名但不包含方法体
  1. 案例代码
function printLabel(labelObject: { label: string }) {
  console.log(labelObject.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
/*********************************/
type myType = {
  name: string;
  age: number;
};

let xm: myType = {
  name: "xiaoming",
  age: 5,
};
/********************************************* */
interface myInterface {
  name: string;
  age: number;
}
interface myInterface {
  gender: string;
}

let objXh: myInterface = {
  name: "xiaohong",
  age: 6,
  gender: "female",
};
  1. 代码分析
    1. 类型检查器会查看printLabel的调用。其中有一个对象参数labelObject
    2. 要求这个对象参数有一个label属性,值为string类型
  2. 接口代码
interface labelValue {
    label: string
}
function printLabel(labelObject: labelValue){
    console.log(labelObject.label)
}
let myobj2 = { size: 11, label: "Size 11 Object" }
printLabel(myObj2)

2.2 可选属性

  1. 接口里的属性不全都是必须的。

2.3 实现接口

  1. 定义类时可以使用implements去实现一个接口。(实现接口就是使类满足接口的要求)
  2. 案例代码
interface ClockInterface {
  name: string;
  setTime(): void;
}

class Clock implements ClockInterface {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  setTime() {}
}
  1. 接口与抽象类的区别
    1. 抽象类中,可以有抽象方法也可以有普通方法。接口中都是抽象方法
    2. 在使用抽象类的时候使用的是extends实现,接口使用的是implements实现
    3. 接口定义一个标准,限制类,符合接口的标准

3. 属性的封装

  1. 通过对属性的控制,放置实例对象直接更改属性值
  2. 案例代码
class Heros {
  private _name: string;
  private _age: number;
  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }
  getName() {
    return this._name;
  }
  setName(value: string) {
    this._name = value;
  }
  setAge(value: number) {
    if (value >= 0) this._age = value;
  }
}

const zs = new Heros("zs", 18);
console.log(zs);
zs.setName("lisi");
console.log(zs.getName());
zs.setAge(10);
console.log(zs);
  1. 分析

    1. 通过设置get和set,控制实例对象直接更改属性值
  2. TS中设置get/set方法

class Heros {
  private _name: string;
  private _age: number;
  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }
  // TS中提供的设置修改属性的get/set方法
  get name() {
    console.log("get name()执行了");
    return this._name;
  }
  set name(value: string) {
    this._name = value;
  }
  get age() {
    return this._age;
  }
  set age(value: number) {
    if (value >= 0) {
      this._age = value;
    } else {
      console.log("更改的年龄不合法");
    }
  }
  setAge(value: number) {
    if (value >= 0) this._age = value;
  }
}
// 实例化对象
const zs = new Heros("zs", 18);
zs.age = 33;
zs.name = "悟空";
console.log(zs);

4. 泛型

  1. 可以使用泛型来创建可重用的组件

4.1 使用泛型的例子

  1. 普通函数案例代码
// 不使用泛型的函数
function fn(arg: number): number {
  return arg
}

// 使用any类型来定义函数
function identity(arg: any): any{
  return arg
}
// 使用类型变量
function ident<T>(arg: T): T {
  return arg
}
  1. 代码分析

    1. 使用any类型当作函数的返回值和参数类型,传入的值和返回值类型是相同的。
    2. 不管传入任何类型的值都会被返回
    3. 使用类型变量,表示类型而不是值,使返回值的类型与传入参数的类型是相同的。
  2. 使用泛型的两种方式

// 第一种
const result1 = ident<string>("hello...");
// 第二种
const result2 = ident("world...");
console.log(result1);
console.log(result2);
  1. 分析
    1. 第一种使用泛型的方式:传入了所有的参数,包括参数类型
    2. 明确的指定了T是string类型
    3. 第二种使用泛型的方式: 利用了类型推论,即编译器会根据传入的参数自动地帮助我们确认T的类型