关于前端:TypeScript-之-Class上

4次阅读

共计 10393 个字符,预计需要花费 26 分钟才能阅读完成。

TypeScript 的官网文档早已更新,但我能找到的中文文档都还停留在比拟老的版本。所以对其中新增以及订正较多的一些章节进行了翻译整顿。

本篇翻译整顿自 TypeScript Handbook 中「Classes」章节。

本文并不严格依照原文翻译,对局部内容也做了解释补充。

类(Classes)

TypeScript 齐全反对 ES2015 引入的 class 关键字。

和其余 JavaScript 语言个性一样,TypeScript 提供了类型注解和其余语法,容许你表白类与其余类型之间的关系。

类成员(Class Members)

这是一个最根本的类,一个空类:

class Point {}

这个类并没有什么用,所以让咱们增加一些成员。

字段(Fields)

一个字段申明会创立一个公共(public)可写入(writeable)的属性:

class Point {
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;

留神:类型注解是可选的,如果没有指定,会隐式的设置为 any。​

字段能够设置初始值(initializers):

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

就像 constletvar,一个类属性的初始值会被用于推断它的类型:

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

–strictPropertyInitialization

strictPropertyInitialization 选项管制了类字段是否须要在构造函数里初始化:

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

留神,字段须要在构造函数本身进行初始化。TypeScript 并不会剖析构造函数里你调用的办法,进而判断初始化的值,因为一个派生类兴许会笼罩这些办法并且初始化成员失败:

class BadGreeter {
  name: string;
  // Property 'name' has no initializer and is not definitely assigned in the constructor.
  setName(): void {this.name = '123'}
  constructor() {this.setName();
  }
}

如果你执意要通过其余形式初始化一个字段,而不是在构造函数里(举个例子,引入内部库为你补充类的局部内容),你能够应用明确赋值断言操作符(definite assignment assertion operator)!:

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

readonly

字段能够增加一个 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.

构造函数(Constructors)

类的构造函数跟函数十分相似,你能够应用带类型注解的参数、默认值、重载等。

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

就像在 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);
        // 'super' must be called before accessing 'this' in the constructor of a derived class.
    super();}
}

遗记调用 super 是 JavaScript 中一个简略的谬误,然而 TypeScript 会在须要的时候揭示你。

办法(Methods)

类中的函数属性被称为办法。办法跟函数、构造函数一样,应用雷同的类型注解。

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

除了规范的类型注解,TypeScript 并没有给办法增加任何新的货色。

留神在一个办法体内,它仍然能够通过 this. 拜访字段和其余的办法。办法体内一个未限定的名称(unqualified name,没有明确限定作用域的名称)总是指向闭包作用域里的内容。

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";
        // Type 'string' is not assignable to type 'number'.
  }
}

Getters / Setter

类也能够有存取器(accessors):

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

TypeScript 对存取器有一些非凡的推断规定:

  • 如果 get 存在而 set 不存在,属性会被主动设置为 readonly
  • 如果 setter 参数的类型没有指定,它会被推断为 getter 的返回类型
  • getters 和 setters 必须有雷同的成员可见性(Member Visibility)。

从 TypeScript 4.3 起,存取器在读取和设置的时候能够应用不同的类型。

class Thing {
  _size = 0;
 
  // 留神这里返回的是 number 类型
  get size(): number {return this._size;}
 
  // 留神这里容许传入的是 string | number | boolean 类型
  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;
  }
}

索引签名(Index Signatures)

类能够申明索引签名,它和对象类型的索引签名是一样的:

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

因为索引签名类型也须要捕捉办法的类型,这使得并不容易无效的应用这些类型。通常的来说,在其余中央存储索引数据而不是在类实例自身,会更好一些。

类继承(Class Heritage)

JavaScript 的类能够继承基类。

implements 语句(implements Clauses)

你能够应用 implements 语句查看一个类是否满足一个特定的 interface。如果一个类没有正确的实现 (implement) 它,TypeScript 会报错:

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 {

注意事项(Cautions)

implements 语句仅仅查看类是否依照接口类型实现,但它并不会扭转类的类型或者办法的类型。一个常见的谬误就是认为 implements 语句会扭转类的类型——然而实际上它并不会:

interface Checkable {check(name: string): boolean;
}
 
class NameChecker implements Checkable {check(s) {
         // Parameter 's' implicitly has an 'any' type.
    // Notice no error here
    return s.toLowercse() === "ok";
                    // any
}

在这个例子中,咱们可能会认为 s 的类型会被 checkname: string 参数影响。实际上并没有,implements 语句并不会影响类的外部是如何查看或者类型推断的。

相似的,实现一个有可选属性的接口,并不会创立这个属性:

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 语句(extends Clauses)

类能够 extend 一个基类。一个派生类有基类所有的属性和办法,还能够定义额定的成员。

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

覆写属性(Overriding Methods)

一个派生类能够覆写一个基类的字段或属性。你能够应用 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");

派生类须要遵循着它的基类的实现。

而且通过一个基类援用指向一个派生类实例,这是十分常见并非法的:

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

然而如果 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();

初始化程序(Initialization Order)

有些状况下,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 的值,因为此时派生类字段初始化还没有运行。

继承内置类型(Inheriting Built-in Types)

留神:如果你不打算继承内置的类型比方 ArrayErrorMap 等或者你的编译指标是 ES6/ES2015 或者更新的版本,你能够跳过这个章节。

在 ES2015 中,当调用 super(...) 的时候,如果构造函数返回了一个对象,会隐式替换 this 的值。所以捕捉 super() 可能的返回值并用 this 替换它是十分有必要的。

这就导致,像 ErrorArray 等子类,兴许不会再如你冀望的那样运行。这是因为 ErrorArray 等相似内置对象的构造函数,会应用 ECMAScript 6 的 new.target 调整原型链。然而,在 ECMAScript 5 中,当调用一个构造函数的时候,并没有办法能够确保 new.target 的值。其余的降级编译器默认也会有同样的限度。

对于一个像上面这样的子类:

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

你兴许能够发现:

  1. 对象的办法可能是 undefined,所以调用 sayHello 会导致谬误
  2. 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;}
}

不过,任何 MsgError 的子类也不得不手动设置原型。如果运行时不反对 Object.setPrototypeOf,你兴许能够应用 __proto__

可怜的是,这些计划并不会能在 IE 10 或者之前的版本失常运行。解决的一个办法是手动拷贝原型中的办法到实例中(就比方 MsgError.prototypethis),然而它本人的原型链仍然没有被修复。

成员可见性(Member Visibility)

你能够应用 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.

受爱护成员的公开(Exposure of protected members)

派生类须要遵循基类的实现,然而仍然能够抉择公开领有更多能力的基类子类型,这就包含让一个 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 修饰符。

穿插等级受爱护成员拜访(Cross-hierarchy protected access)

不同的 OOP 语言在通过一个基类援用是否能够非法的获取一个 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: 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++ 这边。因为 Derived2x 应该只有从 Derived2 的子类拜访才是非法的,而 Derived1 并不是它们中的一个。此外,如果通过 Derived1 拜访 x 是不非法的,通过一个基类援用拜访也应该是不非法的。

看这篇《Why Can’t I Access A Protected Member From A Derived Class?》,解释了更多 C# 这样做的起因。

private

private 有点像 protected,然而不容许拜访成员,即使是子类。

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

穿插实例公有成员拜访(Cross-instance private access)

不同的 OOP 语言在对于一个类的不同实例是否能够获取彼此的 private 成员上,也是不统一的。像 Java、C#、C++、Swift 和 PHP 都是容许的,Ruby 是不容许。

TypeScript 容许穿插实例公有成员的获取:

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

正告(Caveats)

privateprotected 仅仅在类型查看的时候才会强制失效。

这意味着在 JavaScript 运行时,像 in 或者简略的属性查找,仍然能够获取 private 或者 protected 成员。

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

private 容许在类型查看的时候,通过方括号语法进行拜访。这让比方单元测试的时候,会更容易拜访 private 字段,这也让这些字段是弱公有(soft private)而不是严格的强制公有。

class MySafe {private secretKey = 12345;}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
// Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);

不像 TypeScript 的 private,JavaScript 的公有字段(#)即使是编译后仍然保留公有性,并且不会提供像下面这种方括号获取的办法,这让它们变得强公有(hard private)。

class Dog {
  #barkAmount = 0;
  personality = "happy";
 
  constructor() {}
}
"use strict";
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,或者公有字段。然而留神,这也会在运行时影响性能。

TypeScript 系列

  1. TypeScript 之 根底入门
  2. TypeScript 之 常见类型(上)
  3. TypeScript 之 常见类型(下)
  4. TypeScript 之 类型收窄
  5. TypeScript 之 函数
  6. TypeScript 之 对象类型
  7. TypeScript 之 泛型
  8. TypeScript 之 Keyof 操作符
  9. TypeScript 之 Typeof 操作符
  10. TypeScript 之 索引拜访类型
  11. TypeScript 之 条件类型
  12. TypeScript 之 映射类型
  13. TypeScript 之模板字面量类型

微信:「mqyqingfeng」,加我进冴羽惟一的读者群。

如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。

正文完
 0