今天是正月初三,许下三个心愿,一愿家人安康,亲人在旁,二愿山河无恙,人间皆安,三愿喜乐无忧,生活明朗,愿你好事接二连三,新年福气相伴!
目录
一,类成员
这是最基本的类 - 一个空的:
class YuanZhen{
}
1.1 字段
字段声明在类上创建公共可写属性:
class YuanZhen{
name:string
age:number
}
const yuanZhen = new YuanZhen();
yuanZhen.name="袁震"
yuanZhen.age=30
与其他位置一样,类型注释是可选的,但如果未指定,则为隐式 any
。
字段也可以有初始化器;这些将在实例化类时自动运行:
如果你打算通过构造函数以外的方式明确地初始化一个字段(例如,可能一个外部库正在为你填充你的类的一部分),你可以使用明确的赋值断言运算符,!
:
class YuanZhen{
name!:string
age:number
}
1.2 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.
1.3 构造器
类构造函数与函数非常相似。你可以添加带有类型注释、默认值和重载的参数:
class YuanZhen{
name!:string
age:number
constructor(name = "袁震", age = 20) {
this.name = name
this.age = age
}
}
class YuanZhen{
name!:string
age:number
constructor(name:string, age :number)
constructor(name :string)
constructor() {
}
}
类构造函数签名和函数签名之间只有一些区别:
1,构造函数不能有类型参数 - 这些属于外部类声明
2,构造函数不能有返回类型注释 - 类实例类型始终是返回的内容
超类调用
就像在 JavaScript 中一样,如果你有一个基类,在使用任何 this.
成员之前,你需要在构造函数主体中调用 super();
class YuanZhen{
name!:string
age:number
constructor(name:string, age :number)
constructor(name :string)
constructor() {
}
}
class Yz extends YuanZhen{
constructor(name:string, age :number){
//Constructors for derived classes must contain a 'super' call. <ArkTSCheck>
// super(name,age)
}
}
1.4 方法
类上的函数属性称为方法。方法可以使用所有与函数和构造函数相同的类型注释:
class YuanZhen{
name!:string
age:number
constructor(name:string, age :number)
constructor(name :string)
constructor() {
}
work(){
console.info("name="+this.name+"age="+this.age)
}
}
请注意,在方法体内,仍然必须通过 this.
访问字段和其他方法。方法主体中的非限定名称将始终引用封闭作用域内的某些内容
1.5 获取器/设置器
类也可以有访问器:
class YuanZhen{
name!:string
age:number
constructor(name:string, age :number)
constructor(name :string)
constructor() {
}
work(){
console.info("name="+this.name+"age="+this.age)
}
getName():string{
return this.name
}
setName(name:string){
this.name=name
}
}
注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果你不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。
TypeScript 对访问器有一些特殊的推断规则:
1.如果 get
存在但没有 set
,则属性自动为 readonly
2.如果不指定 setter 参数的类型,则从 getter 的返回类型推断
3.getter 和 setter 必须有相同的 成员可见性
1.6 索引签名
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
因为索引签名类型还需要捕获方法的类型,所以要有效地使用这些类型并不容易。通常最好将索引数据存储在另一个地方而不是类实例本身。
二,类继承
与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。
2.1 implements
可以使用 implements
子句来检查一个类是否满足特定的 interface
。如果一个类未能正确实现它,则会触发错误:
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
注意事项
重要的是要理解 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.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;
//Property 'y' does not exist on type 'C'.
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.
语法来访问基类方法。请注意,因为 JavaScript 类是一个简单的查找对象,所以没有 “超级字段” 的概念。
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();
仅类型字段声明
父类构造函数完成后初始化类字段,覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能会成为问题。为了处理这些情况,你可以写 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);
}
}
初始化顺序
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();
这里发生了什么?
1.基类字段被初始化
2.基类构造函数运行
3.派生类字段被初始化
4.派生类构造函数运行
这意味着基类构造函数在其自己的构造函数中看到了自己的 name
值,因为派生类字段初始化尚未运行。
三,成员可见性
3.1 public
类成员的默认可见性为 public
。public
成员可以在任何地方访问:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
因为 public
已经是默认的可见性修饰符,所以你不需要在类成员上编写它,但出于样式/可读性的原因可能会选择这样做。
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();
//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
导出 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
请注意,Derived
已经能够自由读写 m
,因此这并没有有意义地改变这种情况下的 “security”。这里需要注意的主要一点是,在派生类中,如果这种暴露不是故意的,我们需要小心重复 protected
修饰符。
跨层级 protected
访问
不同的 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: Derived1) {
other.x = 10;
//Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
}
}
例如,Java 认为这是合法的。另一方面,C# 和 C++ 选择此代码应该是非法的。
TypeScript 在这里支持 C# 和 C++,因为在 Derived2
中访问 x
应该只在 Derived2
的子类中是合法的,而 Derived1
不是其中之一。此外,如果通过 Derived1
引用访问 x
是非法的(当然应该如此!),那么通过基类引用访问它永远不会改善这种情况。
3.3 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;
}
四,静态成员
类可能有 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'.
静态成员也被继承:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
4.1 特殊静态名称
从 Function
原型覆盖属性通常是不安全/不可能的。因为类本身就是可以用 new
调用的函数,所以不能使用某些 static
名称。name
、length
和 call
等函数属性无法定义为 static
成员:
class S {
static name = "S!";
//Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
4.2 为什么没有静态类
TypeScript(和 JavaScript)没有一个名为 static class
的构造,就像 C# 一样。
这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为 TypeScript 中不存在该限制,所以不需要它们。只有一个实例的类通常只表示为 JavaScript/TypeScript 中的普通对象。
例如,我们不需要 TypeScript 中的 “静态类” 语法,因为常规对象(甚至顶层函数)也可以完成这项工作:
// 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> {
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.
}
请记住,类型总是被完全擦除!在运行时,只有一个 Box.defaultValue
属性槽。这意味着设置 Box<string>.defaultValue
(如果可能的话)也会更改 Box<number>.defaultValue
- 不好。泛型类的 static
成员永远不能引用类的类型参数。
七,类运行时的 this
TypeScript 不会改变 JavaScript 的运行时行为,并且 JavaScript 以具有一些特殊的运行时行为而闻名。
箭头函数
如果你有一个经常以丢失其 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());
这有一些权衡:
1.this
值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此
2.这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本
3.你不能在派生类中使用 super.getName
,因为原型链中没有条目可以从中获取基类方法
八,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
型防护
你可以在类和接口中的方法的返回位置使用 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
:
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
}
九,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.
我们不能用 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
}