TypeScript系列教程八《类》

TypeScript完全支持ES2015中引入的class关键字。
与其他JavaScript语言特性一样,TypeScript添加了类型注释和其他语法,允许您表达类和其他类型之间的关系。

类的成员


下面是一个空的类:

class Point {}

这个类现在毫无用处,现在往这个类里面加点成员

字段

在类上声明字段,编程了可读写的public 属性

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

const pt = new Point();
pt.x = 0;
pt.y = 0;

属性初始化: 类实例化的时候,会初始赋值

class Point {
  x = 0;
  y = 0;
}

const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);

与const、let和var一样,类属性的初始值设定项将用于推断其类型:

const pt = new Point();
pt.x = "0";
//Type 'string' is not assignable to type 'number'.

–strictPropertyInitialization

StricPropertyInitialization设置控制类字段是否需要在构造函数中初始化。

在这里插入图片描述

正常构建需要初始化:

class GoodGreeter {
  name: string;

  constructor() {
    this.name = "hello";
  }
}

不初始化使用!断言,也不会报错:

class OKGreeter {
    // Not initialized, but no error
    name!: string;
  }

readonly

只读属性,不多介绍,只能读取不能赋值。

注意:构造函数内可以赋值

class Greeter {
  readonly name: string = "world";

  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }

  err() {
    this.name = "not ok";
//Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok";
//Cannot assign to 'name' because it is a read-only property.

构造函数

类构造函数与函数非常相似。可以添加带有类型注释、默认值和重载的参数:

class Point {
  x: number;
  y: number;

  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}
class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

super 调用

如果有基类,必须在构造函数中调用super,且在使用this之前

class Base {
  k = 4;
}

class Derived extends Base {
  constructor() {
    // Prints a wrong value in ES5; throws exception in ES6
    console.log(this.k);
//'super' must be called before accessing 'this' in the constructor of a derived class.
    super();
  }
}

Methods

在类中函数属性统称为方法

class Point {
  x = 10;
  y = 10;

  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}
let x: number = 0;

class C {
  x: string = "hello";

  m() {
    // This is trying to modify 'x' from line 1, not the class property
    this.x = "world";
    x = 4 
// Type 'string' is not assignable to type 'number'.
  }
}

Getters / Setters

类也可以有访问器:

class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

注意,没有额外逻辑的字段支持的get/set对在JavaScript中很少有用。如果在get/set操作期间不需要添加额外的逻辑,那么公开公共字段就可以了。

TypeScript对访问器有一些特殊的推理规则:

  • 只有get 没有set ,这个属性自动变成raedonly
  • 如果set 的参数没有明确指出,那么按照get 类型推断
  • Getters and setters必须具有相同的成员可见性(public,private)
class Thing {
    _size = 0;

    get size(): number {
        return this._size;
    }

    set size(value: string | number | boolean) {
        let num = Number(value);

        // Don't allow NaN, Infinity, etc

        if (!Number.isFinite(num)) {
            this._size = 0;
            return;
        }

        this._size = num;
    }
}

索引签名

类可以声明索引签名;它们的工作方式与其他对象类型的索引签名相同:

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  check(s: string) {
    return this[s] as boolean;
  }
}

因为索引签名类型还需要捕获方法的类型,所以很难有效地使用这些类型。一般来说,最好将索引数据存储在另一个地方,而不是类实例本身。

类的实现和继承

与其他具有面向对象特性的语言一样,JavaScript中的类可以从基类继承。

implements

一个类可以准守一个或者多个接口去实现它:

interface Pingable {
  ping(): void;
}

class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

class Ball implements Pingable {
//Class 'Ball' incorrectly implements interface 'Pingable'.
  //Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
  pong() {
    console.log("pong!");
  }
}

类同时可以准守多个接口取实现,例如 class C implements A, B {

可选值不会要求,实现类去实现:

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.

extends (继承)

类可以从基类扩展。派生类具有其基类的所有属性和方法,还定义其他成员。

class Animal {
  move() {
    console.log("Moving along!");
  }
}

class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}

const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
方法重写

子类继承父类之后,可以重写属性和方法

class Base {
  greet() {
    console.log("Hello, world!");
  }
}

class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}

const d = new Derived();
d.greet();
d.greet("reader");

面向对象的特征父类可以指向子类(变量多态):

// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
初始化顺序

在某些情况下,JavaScript类的初始化顺序可能令人惊讶。让我们来考虑这个代码:

class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}

class Derived extends Base {
  name = "derived";
}

// Prints "base", not "derived"
const d = new Derived();

这里发生了什么?

JavaScript定义的类初始化顺序是:

  • 基类字段已初始化
  • 基类构造函数运行
  • 派生类字段已初始化
  • 派生类构造函数运行

这意味着基类构造函数在自己的构造函数中看到了自己的name值,因为派生类字段初始化尚未运行。

类成员访问权限

您可以使用TypeScript来控制某些方法或属性是否对类之外的代码可见。

public

类成员的默认权限就是public,public是任何地方都可以访问:

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

因为public已经是默认的可见性修饰符,所以您不需要在类成员上编写它,但是出于样式/可读性的原因,您可以选择这样做。

protected

protected只有类和子类可以访问

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}

class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
暴露保护成员

派生类需要准守基类的约定,同时派生类也可以更改重写的成员访问权限。

class Base {
    protected m = 10;
    protected n = 11;
  }
  class Derived extends Base {
    // No modifier, so default is 'public'
    m = 15;
  }
  const d = new Derived();
  console.log(d.m); // OK
  console.log(d.n); // 受保护

请注意,Derived已经能够自由地读写m,因此这并不能有意义地改变这种情况的“安全性”。这里要注意的主要事情是,在派生类中,如果不是有意的,我们需要小心地重复受保护的修饰符。

跨层次保护访问

对于通过基类引用访问受保护成员是否合法,不同的OOP语言存在分歧:

class Base {
    protected x: number = 1;
  }
  class Derived1 extends Base {
    protected x: number = 5;
  }
  class Derived2 extends Base {
    f1(other: Derived2) {
      other.x = 10;
    }
    f2(other: Base) {
      other.x = 10;
  //Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
    }
  }

例如,Java认为这是合法的。另一方面,C++和C++选择了这个代码应该是非法的。

TypeScript和C++,C#一样。 C# 有解释为什么要这么做:

Why Can’t I Access A Protected Member From A Derived Class?

private

private 不允许外部访问

class Base {
  private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
//Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.
  }
}

因为private 不允许访问,所以子类继承无法重写

class Base {
  private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'.
  //Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}
跨实例访问private 成员

对于同一类的不同实例是否可以访问彼此的私有成员,不同的OOP语言存在分歧。虽然java、C++、C++、SWIFT和PHP这样的语言允许这样做,但Ruby却没有。

TypeScript允许跨实例私有访问:

class A {
  private x = 10;

  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}
注意事项

与TypeScript类型系统的其他方面一样,只有在类型检查期间才强制执行私有和受保护的。这意味着JavaScript运行时构造(如in或简单属性查找)仍然可以访问私有或受保护的成员:

class MySafe {
  private secretKey = 12345;
}
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

如果需要保护类中的值不受恶意参与者的攻击,则应该使用提供硬运行时隐私的机制,例如闭包、弱映射或私有字段。

静态成员

类可能有静态成员。这些成员与类的特定实例没有关联。可以通过类构造函数对象本身访问它们:

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

静态成员还可以使用public、protected和private可见性修饰符:

class MyClass {
  private static x = 0;
}
console.log(MyClass.x);
//Property 'x' is private and only accessible within class 'MyClass'.

静态成员也会被继承:

class Base {
    static getGreeting() {
      return "Hello world";
    }
  }
  class Derived extends Base {
  }

  let myGreeting = Derived.getGreeting();
特殊静态名称

name,length,call等原型属性(prototype)不能当做静态属性

class S {
  static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
为什么没有静态类?

TypeScript(和JavaScript)不像C#和Java那样有一个称为静态类的构造。

这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为TypeScript中不存在这种限制,所以不需要它们。在JavaScript/TypeScript中,只有一个实例的类通常表示为普通对象。

Java静态类参考:

public class MainInStaticClass {
	static class Main{
		static void main() {
			//将主方法写到静态内部类中,从而不必为每个源文件都这种一个类似的主方法
			new MainInStaticClass().print();
		}
	}
	public static void main(String[] args){
		new MainInStaticClass().print();
	}
	public void print(){
		System.out.println("main in static inner class");
	}
}
public class TestMain {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// new MainInStaticClass().print();
		MainInStaticClass.Main.main();
		new MainInStaticClass.Main();
	}
}

例如,我们不需要TypeScript中的“static class”语法,因为常规对象(甚至顶级函数)也可以完成这项工作:

// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}

// Preferred (alternative 1)
function doSomething() {}

// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

 

泛型类

类,很像接口,可以是泛型的。用new实例化泛型类时,其类型参数的推断方式与函数调用中的相同:

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}

const b = new Box("hello!");
静态属性中使用泛型

下面代码是不合法的:

class Box<Type> {
  static defaultValue: Type;
//Static members cannot reference class type parameters.
}

ts是类型擦除的,变量在运行时中,只有一个类型。成员类型的确定在实例中推断出来,静态属性的分配在于实例初始化之前,无法确定类型。 (猜测,请大神推翻告知)

this 在类中的运行时

重要的是要记住,TypeScript不会改变JavaScript的运行时行为,JavaScript以具有一些特殊的运行时行为而闻名。

JavaScript对此的处理确实不同寻常:

class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};

// Prints "obj", not "MyClass"
console.log(obj.getName());

长话短说,默认情况下,函数中的值取决于函数的调用方式。在这个例子中,因为函数是通过obj引用调用的,所以它的值是obj而不是类实例。

这很少是你想发生的事!TypeScript提供了一些减轻或防止此类错误的方法。

箭头函数

如果您有一个函数经常被调用,而这种调用方式会丢失其上下文,那么使用arrow函数属性而不是方法定义是有意义的:

class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  };
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());

这有一些取舍:

  • this这个值在TS中没有类型检查,在运行时才能确定

  • 这将使用更多的内存,因为每个类实例都有自己的副本,每个函数都是这样定义的

  • 不能在派生类中使用super.getName,因为原型链中没有从中获取基类方法的条目

this参数

在方法或函数定义中,名为this的初始参数在TypeScript中有特殊意义。这些参数在编译过程中被删除:

// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}
// JavaScript output
function fn(x) {
  /* ... */
}

TypeScript检查使用this参数调用函数是否在正确的上下文中完成。我们不需要使用arrow函数,而是可以向方法定义中添加this参数,以静态地强制正确调用方法:

class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();

// Error, would crash
const g = c.getName;
console.log(g());

此方法采用与箭头函数方法相反的折衷方法:

  • JavaScript调用程序可能仍然错误地使用class方法而没有意识到它
  • 每个类定义只分配一个函数,而不是每个类实例分配一个函数
  • 基本方法定义仍然可以通过super调用。
this类型

在类中,称为this的特殊类型动态地引用当前类的类型。让我们看看这有多有用:

class Box {
  contents: string = "";
  set(value: string) {
  
//(method) Box.set(value: string): this
    this.contents = value;
    return this;
  }
}

这里,TypeScript推断set的返回类型是this,而不是Box。现在让我们创建Box的子类:

class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}

const a = new ClearableBox();
const b = a.set("hello");
     
const b: ClearableBox

你也可以在参数标注中使用this类型

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

这与编写other:Box不同-如果您有一个派生类,那么它的sameAs方法现在只接受该派生类的其他实例:

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

class DerivedBox extends Box {
  otherContent: string = "?";
}

const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
//Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
//  Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
this基础类型保护

对于类和接口中的方法,可以在返回位置使用这个is类型。当与类型缩小(例如if语句)混合时,目标对象的类型将缩小到指定的类型。

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}

class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}

class Directory extends FileSystemObject {
  children: FileSystemObject[];
}

interface Networked {
  host: string;
}

const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");

if (fso.isFile()) {
  fso.content;
  
const fso: FileRep
} else if (fso.isDirectory()) {
  fso.children;
  
const fso: Directory
} else if (fso.isNetworked()) {
  fso.host;
  
const fso: Networked & FileSystemObject
}

基于this的类型保护的一个常见用例是允许对特定字段进行延迟验证。例如,当hasValue被验证为true时,这种情况会从保存在框中的值中删除一个未定义的值:

class Box<T> {
  value?: T;

  hasValue(): this is { value: T } {
    return this.value !== undefined;
  }
}

const box = new Box();
box.value = "Gameboy";

box.value;
     
//(property) Box<unknown>.value?: unknown

if (box.hasValue()) {
  box.value;
       
//(property) value: unknown
}

参数属性

TypeScript提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些称为参数属性,通过在构造函数参数前面加上可见性修饰符public、private、protected或readonly来创建。结果字段获取这些修饰符:

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x);
             
(property) Params.x: number
console.log(a.z);

 

类表达式

类表达式与类声明非常相似。唯一真正的区别是类表达式不需要名称,尽管我们可以通过它们最终绑定到的任何标识符来引用它们:

const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};

const m = new someClass("Hello, world");

 

抽象类和成员


TypeScript中的类、方法和字段可以是抽象的。

抽象方法或抽象字段是尚未提供实现的方法或字段。这些成员必须存在于不能直接实例化的抽象类中。

抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,就说它是具体的。

让我们看一个例子

abstract class Base {
  abstract getName(): string;

  printName() {
    console.log("Hello, " + this.getName());
  }
}

const b = new Base();
//Cannot create an instance of an abstract class.

我们不能用new实例化Base,因为它是抽象的。相反,我们需要生成一个派生类并实现抽象成员:

class Derived extends Base {
  getName() {
    return "world";
  }
}

const d = new Derived();
d.printName();

请注意,如果忘记实现基类的抽象成员,则会出现错误:

class Derived extends Base {
//Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
  // forgot to do anything
}
抽象构造签名

有时您希望接受某个类构造函数,该构造函数生成从某个抽象类派生的类的实例。

例如,您可能希望编写以下代码:

abstract class Base {
  abstract getName(): string;

  printName() {
    console.log("Hello, " + this.getName());
  }
}

function greet(ctor: typeof Base) {
  const instance = new ctor();
  instance.printName();
}

TypeScript正确地告诉您,您正试图实例化一个抽象类。毕竟,根据greet的定义,编写这段代码是完全合法的,它最终会构造一个抽象类:

// Bad!
greet(Base);

作为替代,您需要编写一个函数来接受具有构造签名的内容:

function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
greet(Base);
//Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
//Cannot assign an abstract constructor type to a non-abstract constructor type.

 

类之间的关系


在大多数情况下,TypeScript中的类与其他类型在结构上进行比较。

例如,这两个类可以互相替换,因为它们是相同的:

class Point1 {
  x = 0;
  y = 0;
}

class Point2 {
  x = 0;
  y = 0;
}

// OK
const p: Point1 = new Point2();

相同道理,如果类的结构是包含关系,超集可以转向子集类型:

class Person {
  name: string;
  age: number;
}

class Employee {
  name: string;
  age: number;
  salary: number;
}

// OK
const p: Person = new Employee();

这听起来很简单,但也有一些情况似乎比其他情况更奇怪。

空类没有成员。在结构类型系统中,没有成员的类型通常是其他类型的超类型。所以如果你写了一个空类(不要!),任何东西都可以代替它:

class Empty {}

function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}

// All OK!
fn(window);
fn({});
fn(fn);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星宇大前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值