Typescript类

字段

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

class BadGreeter {
  name: string;
Property 'name' has no initializer and is not definitely assigned in the constructor.
}

解决方案1:

class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

解决方案2:打算通过构造函数意外的方式明确的初始化一个字段(例如,可能一个外部库政委你填充你的类的一部分),你可以使用明确的复制断言运算符!

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.
}
}

构造器

构造器函数与函数相似,你可以添加带有类型注释,默认值和重载的参数:

//类注释和默认值
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
  }
}

注意:类构造函数和函数签名的区别:

  • 构造函数不能有类型参数-他们属于外部类声明,我们稍后会了解
  • 构造函数不能有返回类型注释-类实例类型总是返回的

超类调用

就像在Javascript中一样,如果你有一个基类,在使用任何this.成员之前,你hi需要在构造函数主题中调用super();

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();
  }
}

获取器/设置器

类也可以有访问器:

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

请注意:没有额外逻辑得由字符串支持得get/set对在Javascript中很少有用,如果不需要在get/set操作期间添加其他逻辑,则可以公开公共字符串。

索引签名(?)

类可以声明索引签名,这些工作与其他对象类型得索引签名相同:

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

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

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

类继承

implements 从句,可以检测一个类是否满足特性interface。如果一个类未能正确实现它,就会触发错误:

interface Pingable {
  ping(): void;
}

 
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{...}

注意事项:

重要的是要了解implements子句只是检查类是否被视为接口类型。他不会改变类型或其他方法。一个常见得错误来源假设implements子句会改变类类型-它不会!

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);

覆盖方法

派生类也可以覆盖基类字段或属性。你可以使用super。语法来访问基类方法。请注意,因为Javascript类是一个简单得查找对象,所以没有"super"得概念。Typescript强制派生类始终是基类得子类型。

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");

这里需要注意如果Derived不遵守Base得合同怎么办?

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  // Make this parameter required
  greet(name: string) {
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
  Type '(name: string) => void' is not assignable to type '() => void'.
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

代码编译得时候会会提示崩毁

const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();

仅类型字段声明

当target>=ES2022 或 useDefineForClassFields 为true 时,在弗雷构造函数完成后初始化类字段,覆盖父亲设置得任何值。当你只想为继承得字段重新大亨名更准确的类型时,这可能会成为问题。为了处理这些情况,我们可以使用declare来向Typescript表明这个字段名称不应该有运行时影响

interface Animal {
  dateOfBirth: any;
}
 
interface Dog extends Animal {
  breed: any;
}
 
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
 
class DogHouse extends AnimalHouse {
  // Does not emit JavaScript code,
  // only ensures the types are correct
  declare resident: Dog;
  constructor(dog: Dog) {
    super(dog);
  }
}

初始化顺序

在某些情况下,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值,因为派生类字段初始化还没有运行到。

创建案例

class MsgError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return "hello " + this.message;
  }
}

你可能会发现:

  • 方法可能是构造这些子类返回的对象上的 undefined,所以调用 sayHello 会导致错误。
  • instanceof 将在子类的实例及其实例之间断开,因此 (new MsgError()) instanceof MsgError 将返回 false。

作为建议,你可以在任何 super(...) 调用后立即手动调整原型。

class MsgError extends Error {
  constructor(m: string) {
    super(m);
 
    // Set the prototype explicitly.
    Object.setPrototypeOf(this, MsgError.prototype);
  }
 
  sayHello() {
    return "hello " + this.message;
  }
}

成员可见性

  • public 是默认的课件性修饰符,所以你不需要在类成员中编写它。
  • 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.

在派生类需要遵循基本锲约,但是我们可以选择公开具有更多功能的基类子类型。让protected成员成为public

class Base{
	protected m = 10;
}
class Derived extends Base{
	m = 15;
}

上面的代码Derived已经能够自由读写m,但是需要注意,如果这样更改,其实对于基类的protected就没有什么意义,所以在定义protected时还是建议遵守约定。

跨层级protected访问

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: Derived1) {
    other.x = 10;
Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
  }
}

不同的语言觉得跨阶层合法和不合法不统一,在Typescript认为不允许,跨阶级为非法行为。

  • private

private类似于protected,但不允许从子类访问成员:

// @errors: 2341
class Base {
  private x = 0;
}
// ---cut---
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
  }
}

//外部也不能直接调用
const b = new Base();
// Can't access from outside the class
console.log(b.x);

因为派生类不可见,所以派生类不能增加他们的可见性

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访问

class A {
  private x = 10;
 
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}

需要注意:Typescript类型系统,仅在类型检测期间强制执行,这就意味着in或简单属性查找之类的Javascript运行时构造仍然可以访问private或protected成员

class MySafe{
	private secretKey = 12345;
}

//如果单纯的在一个js文件中使用。
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

我们在访问属性的时候除了.以外还可以通过[]进行访问。private如果使用了[]仍然会访问到

与Typescript的private不同,Javascript的私有字段(#)在编译后仍然是私有的,并且不提供前面提到的像括号符号访问这样的转义舱口,这是的他们很难私有

class Dog {
  #barkAmount = 0;
  personality = "happy";
 
  constructor() {}
}

当编译到ES2021或者更低版本的时,Typescript将使用WeakMaps代替#

"use strict";
var _Dog_barkAmount;
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();

如果你需要保护类中的值免受恶意行为者的上海,你应该使用提供硬运行时隐私的机制,例如闭包、WeakMaps或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。

静态成员

类有可能static个成员,这些长远不与类得特性实例相关联。他们可以通过类构造函数对象本身访问

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'.

静态名称保护,不能使用一些保护文字例如name,length,call等等

class S {
  static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}

为什么没有静态类?

Typescript没有一个名为static class的构造,我们如果需要静态类可以直接写顶层函数

// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

static类中的块

静态块允许你编写具有自己作用域的语句序列,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有编写语句的所有功能的初始化代码,不会泄露变量,并且可以完全访问我们的类的内部结构

class Foo {
    static #count = 0;
 
    get count() {
        return Foo.#count;
    }
 
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

泛型类

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

静态成员的类型参数

此代码不合法,原因可能并不明显

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

请记住,类型总是被完全擦除! 在运行时,只有一个 Box.defaultValue 属性槽。 这意味着设置 Box<string>.defaultValue(如果可能的话)也会改变 Box<number>.defaultValue - 不好。 泛型类的 static 成员永远不能引用类的类型参数。

类运行时的this

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

箭头函数

为了防止类上的this丢失上下文的方式调用的函数,出现了箭头函数

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 值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此
  • 这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本
  • 你不能在派生类中使用 super.getName,因为原型链中没有条目可以从中获取基类方法

this参数

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

type SomeType = any;
// ---cut---
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}

//javascript output
function fn(x) {
  /* ... */
}

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 的子类:

你还可以在参数类型注释中使用 this:

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

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

this型防护

我们可以再类和接口中的方法的返回位置使用this is type.当与类型缩小(例如if语句)混合时,目标对象的类型将缩小到指定的Type

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被验证为真时,这种情况会从框中保存的值中删除undefined:

参数属性

Typescript提供了特殊的语法,用于将构造函数参数转化为具有相同名称和值的类属性。这些称为参数属性可以使用public、private、protected或readonly来创建

构造函数签名

class Point {
  createdAt: number;
  x: number;
  y: number
  constructor(x: number, y: number) {
    this.createdAt = Date.now()
    this.x = x;
    this.y = y;
  }
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {
  point.x += 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8

这里使用了InstanceType<typeof Point> 用来获取构造函数的返回类型

abstract类和成员

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.

还需要注意如果构造函数中基类成员中拥有抽象方法,继承的时候就必须需要实现。

抽象构造签名

不正确的写法

调用

greet(Base)

正确的写法

但是需要注意Typescript正确的告诉你可以调用那些类构造函数-Derived,因为他是具体的类,但不能使用Base因为他是基类。

类之间的关系

在大多数情况下,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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值