Leon's blog Leon's blog
首页
前端
场景
技术
更多
关于
收藏
索引
GitHub (opens new window)

Leon Yu

做一个有个性的开发者~
首页
前端
场景
技术
更多
关于
收藏
索引
GitHub (opens new window)
  • 技术
  • TypeScript

    • 前言
    • 教程中的难点
    • 教程中的难点2
    • 教程中的难点3
    • NodeJS

    • shell

    • vscode开发

    • 图解HTTP阅读

    • 计算机网络

    • 技术
    • TypeScript
    leon yu
    2024-05-29
    目录

    教程中的难点3

    提示

    本文整理自 TypeScript 官方文档中的 handlebook,版本为 V5.4

    # 类

    # 类的组成部分
    1. 类的字段
    • 字段声明可以在类上创建公共可写属性
    • 可以声明类型,不声明则隐式为any
    • 类的字段在声明时可以给定初始值
    • 设置 strictPropertyInitialization 为 true 则必须给字段初始值
    • 如果不希望初始化,但是又设置了 strictPropertyInitialization,可以使用非空断言
    • 在字段面前加入 readonly 则该字段不能被修改
    class Person {
      name: string
      age // 隐式为any
      gender!: 'male' | 'female' // 设置非空断言时不初始化也不会报错
      readonly privateHobby: string // 设置了 readonly 后无法修改 
    }
    
    1
    2
    3
    4
    5
    6
    1. 类的构造函数
    • 类的构造函数也可以加入带有注释、默认值、重载的参数
    • 类的构造函数不能有类型参数,即无法使用泛型
    • 类的构造函数不能设置返回值的类型
    • 调用 super() 必须要在使用 this 之前
    1. 类的方法
    • 除了类型标注意外,ts的类方法与js无异
    1. 类的 getter 和 setter
    • 对访问器有一些限制
      • get存在而set不存在时默认只读
      • 未指定setter的类型,将会从getter的返回类型进行推断
      • getter和setter必须有相同的成员可见性
      • ts 4.3 开始,getter和setter可以设置不同的参数类型
    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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    • 索引签名,与对象的索引签名类似
    class MyClass {
      [s: string]: boolean | ((s: string) => boolean);
     
      check(s: string) {
        return this[s] as boolean;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7

    注意

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

    # 类的继承
    • implements 子句用以判断 class 是否实现了 interface 中的所有成员

    • implements 实现多个 interface,中间使用逗号隔开

      • 值得注意的时,implements 子句不会完善 class 的类型,一切都还需要自己重新声明
    • extends

      • 当使用 extends 关键字时,派生类会继承基类所有的属性与方法,同时还可以定义其他成员
      • 派生类可以重写基类的方法,如果需要使用基类方法,可以使用 super.method() 的写法
    • 值得注意的一点是,派生类在重写基类方法时,一定要遵循基类的规范,因为在ts的背景下,会使用基类来引用派生类的实例

    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();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    注意

    当 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);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    • JavaScript 类的初始化顺序如下:
      • 基类字段初始化 -> 基类 constructor 运行 -> 派生类字段初始化 -> 派生类 constructor 运行
      • 基类 constructor 运行时已经看到了自己的字段,但是还没有看到派生类的字段

    提示

    extends 有一定的历史兼容性问题 在 ES2015 中,返回对象的构造函数隐式地将 this 的值替换为 super(...) 的任何调用方。生成的构造函数代码必须捕获 super(...) 的任何潜在返回值并将其替换为 this 。 因此,子类化 Error 、 Array 和其他 可能不再按预期工作。这是因为 Error , Array 等的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。默认情况下,其他下层编译器通常具有相同的限制。

    建议在任何 super(...) 调用后立即手动调整原型。

    class MsgError extends Error {
      constructor(m: string) {
        super(m);
     
        // Set the prototype explicitly.
        Object.setPrototypeOf(this, MsgError.prototype);
      }
     
      sayHello() {
        return "hello " + this.message;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    但是, MsgError 的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf 的运行时,您可以改用 __proto__

    遗憾的是,这些解决方法不适用于 Internet Explorer 10 及更早版本。可以手动将方法从原型复制到实例本身(即 MsgError.prototype 到 this ),但原型链本身无法固定。

    # 成员可见性
    # pulblic

    类成员的默认可见性为 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.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    • 派生类可以将基类的 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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    • 如果要保证 m 仍然为 protected 的,需要加入修饰符,否则会默认为 public

    • 不同派生类之间无法通过基类来访问 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) {
        // Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
        other.x = 10;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # private
    • 仅允许类本身访问其 private 可见性的成员

    • ts 允许跨实例访问 private 成员

    class A {
      private x = 10;
     
      public sameAs(other: A) {
        // No error
        return other.x === this.x;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    注意

    与 TypeScript 类型系统的其他方面一样, private 和 protected 仅在类型检查期间强制执行。

    这意味着 JavaScript 运行时构造(如 in 或简单属性查找)仍然可以访问 private 或 protected 成员

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

    与 TypeScripts 的 private 不同,JavaScript 的私有字段 ( # ) 在编译后保持私有,并且不提供前面提到的转义舱口,如括号符号访问,使它们成为硬私有的。

    编译到 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 # 。

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

    # 静态成员

    静态成员不与类的特定实例相绑定,而只能通过类的构造函数对象本身去访问

    class MyClass {
      static x = 0;
      static printX() {
        console.log(MyClass.x);
      }
    }
    console.log(MyClass.x);
    MyClass.printX();
    
    1
    2
    3
    4
    5
    6
    7
    8
    • 静态成员也可以使用 public protected private 来修饰
    • 静态成员是可以继承的
    • 特殊的静态成员。函数的属性,例如 name length call 是无法被设置为静态成员的
    • TS中不需要静态的类,因为只有一个实例的类对应着一个对象
    # 静态代码块

    在静态代码块中,我们可以访问到 private 的属性并可以读写对应的值,这可以帮助我们初始化,完全访问内部变量而不会造成变量泄漏

    class Foo {
      static #count = 0;
     
      get count() {
        return Foo.#count;
      }
     
      static {
        try {
          const lastInstances = loadLastInstances();
          Foo.#count += lastInstances.length;
        }
        catch {}
    }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 泛型类
    • 可以通过在类中加入泛型来控制类中某些成员的类型,泛型可以使用约束和接口来书写
    class Box<Type> {
      contents: Type;
      constructor(value: Type) {
        this.contents = value;
      }
    }
     
    const b = new Box("hello!");
    
    1
    2
    3
    4
    5
    6
    7
    8
    • 静态成员不可以加入泛型,因为静态成员是所有类共用的,不是每个实例私有的
    # 类与 this
    • js 中的 this 取决于如何被调用,当被直接调用时,访问的是类作为 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());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    • 使用箭头函数可以修正上述问题,因为箭头函数没有自己的 this,因此总会使用类中的 this,但是也有一些缺陷

      • 箭头函数不会被绑定在类的 prototype 上,因此,每个实例都会创建自己的箭头函数,从而占用更多的内存
      • 箭头函数无法被继承,只能通过 super.method 来进行访问
    • 也可以通过为函数的 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());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # this 的类型
    • this 总指向调用他的类
    class Box {
      content: string = ''
    
      set (val: string) {
        this.content = val
        return this
      }
    }
    
    class ClearableBox extends Box {
      clear () {
        this.content = ''
      }
    }
    
    const clearableBox = new ClearableBox()
    const anotherClearableBox = clearableBox.set('hello') // anotherClearableBox 的类型将会为 ClearableBox
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    • 如果通过参数闭包了类的 this,将无法传递其他的类
    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'.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 基于 this 的类型守卫
    • 使用类型谓语做类型缩窄
    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
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    • 通常这种方式被用来限定不为 undefined
    class Box<T> {
      value?: T;
     
      hasValue(): this is { value: T } {
        return this.value !== undefined;
      }
    }
     
    const box = new Box<string>();
    box.value = "Gameboy";
     
    box.value;
         
    // (property) Box<string>.value?: string
     
    if (box.hasValue()) {
      box.value;
    // (property) value: string
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 参数修饰符
    • 可以为参数添加 public protected private 等修饰符
    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);
    // Property 'z' is private and only accessible within class 'Params'.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 类表达式

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

    const someClass = class<Type> {
      content: Type;
      constructor(value: Type) {
        this.content = value;
      }
    };
     
    const m = new someClass("Hello, world");
    // const m: someClass<string>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 构造函数签名

    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> // 从这里获取到了 class 类的 实例的类型
     
    function moveRight(point: PointInstance) {
      point.x += 5;
    }
     
    const point = new Point(3, 4);
    moveRight(point);
    point.x; // => 8
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 抽象类和抽象成员

    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.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

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

    class Derived extends Base {
      getName() {
        return "world";
      }
    }
     
    const d = new Derived();
    d.printName();
    
    1
    2
    3
    4
    5
    6
    7
    8
    # 从抽象类派生类中创造实例
    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.
    
    1
    2
    3
    4
    5
    6
    7
    8
    # 类之间的关系
    • 当类的结构相同时,被认为是同一个类

    • 即使类之间没有显式继承,类之间仍然会存在父子关系

    class Person {
      name: string;
      age: number;
    }
     
    class Employee {
      name: string;
      age: number;
      salary: number;
    }
     
    // OK
    const p: Person = new Employee();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    提示

    空类没有成员,可以被任何其他类所替代,但是同样地,空类也没法做任何事情,不建议使用。

    最近更新~: 2024/08/19, 01:05:55
    教程中的难点2
    sequelize学习

    ← 教程中的难点2 sequelize学习→

    最近更新
    01
    应用层
    01-11
    02
    计算机网络和因特网
    12-22
    03
    关于
    12-22
    更多文章>
    Theme by Vdoing | Copyright © 2024-2025 主题来自 Evan Xu | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式