ts类描述(成员、继承、成员可见性、静态成员等)

1.类成员

1.1 最基本的类

class Point{}

1.2 字段

class Point {
	x: number;
	y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
// 类型注释是可选的,未指定,则为隐式any

字段初始化;将在实例化类时自动运行:

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

// 就像 const、let 和 var 一样,类属性的初始化器将用于推断其类型
const pt2 = new Point();
pt2.x = "0"; // error:Type 'string' is not assignable to type 'number'.

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

class BadGreeter {
  name: string; // error: Property 'name' has no initializer and is not definitely assigned in the constructor.
}
// --------------
class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

使用明确的赋值断言运算符:

class OKGreeter {
	name!: string; // 未初始化,但没有错误
}

1.3 readonly

防止对构造函数之外的字段进行赋值

class Greeter {
	readonly name: string = 'world';
	constructor(otherName?: string) {
		if(otherName !== undefined) {
			this.name = otherName;
		}
	}
	err() {
		this.name = 'not ok'; // error: 无法为“name”赋值,因为它是只读属性
	}
}
const g = new Greeter();
g.name = 'also not ok';	// error: 无法为“name”赋值,因为它是只读属性

1.4 构造器

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

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. 成员之前,你需要在构造函数主体中调用 super();:

class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    // Prints a wrong value in ES5; throws exception in ES6
    console.log(this.k); // error: 访问派生类的构造函数中的 "this" 前,必须调用 "super"
    super();
  }
}

1.5 方法

类上的函数属性称为方法。 方法可以使用所有与函数和构造函数相同的类型注释:

class Point {
  x = 10;
  y = 10;
 
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}

在方法体内,仍然必须通过 this. 访问字段和其他方法。 方法主体中的非限定名称将始终引用封闭作用域内的某些内容:

let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    // This is trying to modify 'x' from line 1, not the class property
    x = "world"; // error: 不能将类型“string”分配给类型“number”
  }
}

1.6 获取器/设置器

类也可以有访问器:

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

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

  • 如果 get 存在但没有 set,则属性自动为 readonly
  • 如果不指定 setter 参数的类型,则从 getter 的返回类型推断
  • getter 和 setter 必须有相同的 成员可见性

从 TypeScript 4.3 开始,可以使用不同类型的访问器来获取和设置

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

1.7 索引签名

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
 
  check(s: string) {
    return this[s] as boolean;
  }
}

2. 类继承

2.1 implements

你可以使用 implements 子句来检查一个类是否满足特定的 interface。 如果一个类未能正确实现它,则会触发错误:

interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
// 类“Ball”错误实现接口“Pingable”。
//  类型 "Ball" 中缺少属性 "ping",但类型 "Pingable" 中需要该属性
  pong() {
    console.log("pong!");
  }
}

类也可以实现多个接口,例如 class C implements A, B {。
注意事项
重要的是要理解 implements 子句只是检查类可以被视为接口类型。 它根本不会改变类的类型或其方法。 一个常见的错误来源是假设 implements 子句会改变类类型 - 它不会!

interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) {
// 参数“s”隐式具有“any”类型。
    return s.toLowerCase() === "ok";
  }
}

在这个例子中,我们可能预计 s 的类型会受到 check 的 name: string 参数的影响。 它不是 - implements 子句不会更改检查类主体或推断其类型的方式。

同样,使用可选属性实现接口不会创建该属性:

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
c.y = 10; // error: 类型“C”上不存在属性“y”

2.2 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. 语法来访问基类方法
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");

仅类型字段声明
当 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";
}
 
// 打印:My name is base
const d = new Derived();

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

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

3. 成员可见性

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

3.1 public

类成员的默认可见性为 public。 public 成员可以在任何地方访问:

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

3.2 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(); // error: 属性“getName”受保护,只能在类“Greeter”及其子类中访问。

导出 protected 成员
可以选择公开具有更多功能的基类子类型。 这包括让 protected 成员成为 public:

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

跨层级 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; // error: 属性“x”受保护,只能在类“Derived1”及其子类中访问
  }
}

3.3 private

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

class Base {
  private x = 0;
}
const b = new Base();
console.log(b.x); // error: 属性“x”为私有属性,只能在类“Base”中访问

class Derived extends Base {
  showX() {
    console.log(this.x); // error: 属性“x”为私有属性,只能在类“Base”中访问
  }
}

因为 private 成员对派生类不可见,所以派生类不能增加它们的可见性:

class Base {
  private x = 0;
}
class Derived extends Base {
// 类“Derived”错误扩展基类“Base”。
//   属性“x”在类型“Base”中是私有属性,但在类型“Derived”中不是
  x = 1;
}

跨实例 private 访问

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

警告
private 和 protected 仅在类型检查期间强制执行。
这意味着 in 或简单属性查找之类的 JavaScript 运行时构造仍然可以访问 private 或 protected 成员:

class MySafe {
  private secretKey = 12345;
}

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

private 还允许在类型检查期间使用括号表示法进行访问。 这使得 private 声明的字段可能更容易访问,例如单元测试,缺点是这些字段是软私有的并且不严格执行隐私。

class MySafe {
    private secretKey = 12345;
  }
   
  const s = new MySafe();
   
  // Not allowed during type checking
  console.log(s.secretKey); // error: 属性“secretKey”为私有属性,只能在类“MySafe”中访问
   
  // OK
  console.log(s["secretKey"]);

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

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

4. 静态成员

通过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); // error: 属性“x”为私有属性,只能在类“MyClass”中访问

静态成员也被继承:

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

5. static 类中的块

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

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

6. 泛型类

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

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
 
const b = new Box("hello!");
     // const b: Box<string>

静态成员中的类型参数

class Box<Type> {
  static defaultValue: Type; // 静态成员不能引用类类型参数。
}

7. 类运行时的this

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());
// 默认情况下,函数中 this 的值取决于函数的调用方式

7.1 箭头函数

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,因为原型链中没有条目可以从中获取基类方法

7.2 this参数

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

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

TypeScript 检查是否使用正确的上下文调用带有 this 参数的函数。 我们可以不使用箭头函数,而是在方法定义中添加一个 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()); // error: 类型为“void”的 "this" 上下文不能分配给类型为“MyClass”的方法的 "this"。

此方法与箭头函数方法进行了相反的权衡:

  • JavaScript 调用者可能仍然不正确地使用类方法而没有意识到
  • 每个类定义只分配一个函数,而不是每个类实例一个
  • 仍然可以通过 super 调用基本方法定义。

8. this类型

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

class Box {
  contents: string = "";
  set(value: string) {
  	// set的类型:(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;
  }
}

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
}

9. 参数属性

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); // error: 属性“z”为私有属性,只能在类“Params”中访问

10. 类表达式

const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
 
const m = new someClass("Hello, world");
 // const m: someClass<string>

11. 构造函数签名

JavaScript 类是使用 new 运算符实例化的。 给定类本身的类型,InstanceType 工具类型会对此操作进行建模。

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

12. abstract 类和成员

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

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

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

abstract class Base {
  abstract getName(): string;
 
  printName() {
    console.log("Hello, " + this.getName());
  }
}
 
const b = new Base(); // error:无法创建抽象类的实例

我们不能用 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
  // 非抽象类“Derived”不会实现继承自“Base”类的抽象成员“getName”。
}

12.1 抽象构造签名

有时你想接受一些类构造函数,它产生一个派生自某个抽象类的类的实例。
例如,你可能想编写以下代码:

function greet(ctor: typeof Base) {
  const instance = new ctor(); // error: Cannot create an instance of an abstract class.
  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 正确地告诉你可以调用哪些类构造函数 - Derived 可以,因为它是具体的,但 Base 不能。

13. 类之间的关系

在大多数情况下,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
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值