Class 类
TypeScript 支持ES2015
中引入的关键字class
与其他JavaScript语言功能一样, TypeScript也为class
添加了类型注释和其他语法, 以允许你表达类和其他类型之间的关系
1. class 成员
这是一个基本的类,只是一个空类
class Point{
}
这个类还不是很有用,所以我们开始添加一些成员
1.1. 字段
1.1.1 字段(属性)的理解
字段声明在类上创建公共可写属性
class Point{
// 这些字段在在类实例化后会成为实例对象的属性
x: number
y: number
}
// 实例化
const pt = new Point()
pt.x = 0;
pt.y = 0;
console.log('pt',pt)
/*
{x: 0,y: 0}
*/
类的成员x
, y
为公共的可写属性, 也就是类实例上的属性
与其他位置一样, 类型注释是可选的, 但是如果未指定,将默认为any
类型
class Point{
x; // 类型: (property) Point.x: any
y; // 类型: (property) Point.y: any
}
字段也可以有初始器, 这些将在类被实例化时自动运行
class Point{
x = 0;
y = 0;
// (property) Point.x: number
// (property) Point.y: number
}
const pt = new Point()
console.log(`${pt.x}, ${pt.y}`); // 0 ,0
就像var
,const
, let
定义变量一样, 类属性的初始器也将会用于推断其类型
如果赋值类型不匹配将报错
class Point{
x = 0;
y = 0;
}
const pt = new Point()
pt.x = '0'
// 不能将类型“string”分配给类型“number”
初始化赋值为number类型的值, 实例化后赋予string类型的值将警告
1.1.2 readonly 只读
字段可以使用readonly
修饰符为前缀, 这样可以防止对构造函数之外的字段进行赋值
例如:
class Greeter {
readonly name: string = 'world'
constructor(otherName?:string){
if(otherName !== undefined){
this.name = otherName
}
}
err(){
this.name = 'not ok'
// 报错:无法分配到 "name" ,因为它是只读属性。
}
}
const g = new Greeter()
g.name = 'also not ok'
// 无法分配到 "name" ,因为它是只读属性。
只读属性值可以在constructor
构造函数中进行修改赋值
例如:
const h = new Greeter('world')
console.log('h',h)
// {name: 'world'}
类中初始name
的hello
在构造函数constructor
被调用时修改为world
1.2 构造函数
类的构造函数与普通函数非常相似, 你可以添加带有类型注释, 默认值和重载的参数
例如:
class Person{
name:string;
age:number;
// 构造函数
// name 参数添加了类型注释
// age 参数使用了默认值, 自动推断类型为number
constructor(name:string, age = 18){
// (parameter) age: number
this.name = name
this.age = age
}
}
在构造函数中, name
属性, 添加了类型注释, age
属性添加了默认值
构造函数也可以使用重载
class Person{
name:string;
age:number;
className: number; // 班级
// 重载
// 定义了两个重载, 一个接受两个参数, 第二个接受一个number类型参数
constructor(name:string,age:number)
constructor(className:number);
// 重载的实现
constructor(nameOrClassName:string | number, age = 18){
if(typeof nameOrClassName == 'string'){
this.name = nameOrClassName
}else{
this.className = nameOrClassName
}
this.age = age
}
}
类构造函数签名和函数签名之间只有一些区别:
- 构造函数不能有类型参数-他们属于外部类声明, 稍后了解
- 构造函数不能有返回类型注释- 类实例类型总是返回的
构造函数中super
关键字
注意,就像在JavaScript中一样, 如果有一个基类, 你需要使用super()
来继承基类的属性或方法时, 在 任何this.成员之前调用你的构造函数体
例如
// 基类
class Base{
age = 16
}
// 派生类
class Person extends Base{
constructor(){
// 错误的写法:访问派生类的构造函数中的 "this" 前,必须调用 "super"。
this.age = 20
super()
}
}
在JavaScript中, 忘记调用super
是一个很容易犯的错误, 但TypeScript会在必要时告诉你
例如:
// 派生类
class Person extends Base{
constructor(){
this.age = 20
// 不使用super 报错:
// 派生类的构造函数必须包含 "super" 调用。
}
}
1.3 方法
类上的函数属性称为方法, 方法可以使用所有与函数和构造函数相同的类型注释
例如:
class Person{
name:string
age: number
constructor(name:string, age:number){
this.name = name
this.age = age
}
// 方法(类实例化后,原型上的方法)
sayHello(name:string):void{
console.log(`hello ${name}, 我叫${this.name}`)
}
}
const student = new Person('张三',18)
student.sayHello('李四')
// console: hello 李四, 我叫张三
除了标准的类型注释, TypeScript没有为方法添加任何新的东西
请注意,在方法体内, 仍然必须通过this.
的方式调用主体中非限定名称的成员, 将始终引用封闭范围内的某些内容
例如:
let x:number = 10
class C{
x:string = 'hello'
m(){
x = 'world'
// 不能将类型“string”分配给类型“number”。
// 不使用this.成员的方式, 直接使用x 查找的不是类成员,而是不同的变量x
}
}
示例中调用的x
并不是类主体范围内的x
属性, 而是外部的变量x, 因此不能通过将string类型的值赋值给number类型的变量
1.4 getters / setters
类也可以有访问器
例如:
class C{
_length = 0;
// getter
get length(){
return this._length
}
// setter
set length(value){
this._length = value
}
}
请注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果您不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。
Typescript对访问器有一些特殊的推理规则
- 如果
get
存在但不存在set
, 则属性自动推断为readonly
属性 - 如果不指定
setter
参数的类型, 则从getter
的返回类型推断 -
Getter
和Setter
必须具有相同成员可见性
从TypeScript4.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;
}
}
示例中, 在使用访问器设置参数可以为string, number, boolean的联合类型
但是因为属性是number类型, 所有需要在访问器中通过Number 转换类型
1.5 索引签名
类可以声明索引签名; 这与 其他对象类型的索引签名相同
处理构造函数, 类型中的成员都必须满足索引签名
例如:
class MyClass{
// 索引签名: 属性的值为Boolean类型或一个方法接受一个string类型参数并返回boolean类型
[s:string]: boolean | ((s:string) => boolean)
// 属性
isName = false
// 方法
check(s:string){
return true
}
}
如果不满足索引签名的匹配则报错
class MyClass{
// 索引签名: 属性的值为Boolean类型或一个方法接受一个string类型参数并返回boolean类型
[s:string]: boolean | ((s:string) => boolean)
// 属性
isName = 10
// 报错: 类型“number”的属性“isName”不能赋给“string”索引类型“boolean | ((s: string) => boolean)”。ts(2411)
// 方法
check(s:string){
return this[s]
}
// 报错:类型“(s: string) => boolean | ((s: string) => boolean)”的属性“check”不能赋给“string”索引类型“boolean | ((s: string) => boolean)”
}
索引签名类型还需要捕获方法的类型, 所以要有效的使用这些类型并不容易,
通常最好将索引数据存储在了另一个地方而不是类实例 本身
2. 类的实现
2.1implements
实现
你可以使用implements
关键字来检查一个类是否满足特定接口interface
, 如果一个类未能正确实现这个接口, 则会发错错误
简单说:就是类中成员要实现接口定义成员
例如:
// 接口中声明了一个方法
interface Person{
name: string //接口中的属性
sayHello():void // 接口中的方法
}
// Student 类通过 implements 实现接口 Person 成员
class Student implements Person{
// 实现接口中的属性
name :string
// 实现接口中的方法
sayHello() {
console.log('hello')
}
}
// Doctor 未实现 接口中的sayHello 方法 ,因此 报错
class Doctor implements Person{
say() {
console.log('hello')
}
}
/*
类“Doctor”错误实现接口“Person”。
类型 "Doctor" 中缺少属性 "sayHello",但类型 "Person" 中需要该属性。
*/
示例中Student
实现了接口Person
, 但是Doctor
类没有实现, 所有报错, 告诉接口中需要sayHello
属性, 但Doctor
没有实现它
2.2 类实现多个接口
类也可以实现多个接口, 语法如下:
class C implements A, B {}
// C为类, A,B为接口
例如:
// 接口中声明了一个方法
interface Person{
sayHello():void
}
// 接口
interface Base{
name:string;
age: number;
}
// 类实现两个接口
class Student implements Person,Base{
// name,age 实现 Base接口
name = 'hello';
age = 18
// sayHello 实现Person 接口
sayHello() {
console.log('hello')
}
}
注意事项:
一个很重要的理解:implements
关键字只是检查类是否可以被视为接口类型, 它根本不会改变类的类型或其他方法,一个常见的错误就是假设implement
关键字会改变类的类型, 但是它不会
例如:
// 接口
interface Checkable{
check(name:string):boolean
}
class NameCheck implements Checkable{
check(s) {
// (parameter) s: any
// 注意此时参数那么为any类型, 并不会因为implements, 而导致参数类型发生改变(变为接口定义的参数类型)
return s.toLowercase() === 'ok'
}
}
在这个例子中, 我们可以预计参数s
的类型会受到接口中name:string
参数的影响, 但implements
不会更改检查类主体或推断其类型的方式
因此示例中,参数s
还是按照正常的推断规则, 被推断为any
类型
2.3 接口中存在可选属性
同样, 如果 接口中使用了可选属性,那么在实现接口时不会创建该属性
// 接口,age为可选属性
interface Person{
name: string
age?:number
}
// 1.类实现, age可选属性没有被实现
class Student implements Person{
name: string
}
// 实例化类
const stu = new Student()
// 实例对象上不存在age属性, 因为类没有实现age属性
stu.age = 10
// Doctror实现了接口中的可选属性
class Doctor implements Person{
name: string
age:number
}
// 2.类实现了可选属性,实例对象上就可以操作可选属性
const doc = new Doctor()
doc.age = 20
3. extends 类继承
与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。
类可能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 dog = new Dog()
dog.move()
// Moving along
dog.woof(3)
// 3 woof!
示例中Dog
类虽然没有定义move 方法, 但是其实例对象依然可以使用move方法
原因在于Dog
类是extends
继承基类Animal
类的派生类, 其会自动继承基类的成员
3.1 覆盖方法
派生类也可以覆盖其基类字段或属性, 你可以使用super
语法来访问基类方法,
TypeScript强制派生类始终是其基类的子类型
例如, 下面的这种覆盖方法是合法的方式
// 基类
class Base{
greet(){
console.log('hello world')
}
}
// 派生类
class Derived extends Base {
greet(name?:string){
if(name === undefined){
// 如果没有传递参数, 则调用父类的greet方法
super.greet()
}else{
// 如果有参数, 则使用派生类自己的覆盖
console.log(`Hello, ${name.toUpperCase()}`)
}
}
}
const d = new Derived()
d.greet()
// hello world
d.greet('reader')
// Hello, READER
派生类遵循其基类契约很重要, 请记住, 通过基类类型注释来引用派生类的实例是很常见的(而且总是合法的)
// 通过基类类型注释来应用派生类的实例
const b:Base = d;
b.greet()
// hello world
b.greet('jack')
// Hello, JACK
示例中,变量b
的类型注释是基类Base
, 但是赋值给变量b
的确实派生类的实例对象
如果派生类不遵循基类的契约怎么办?
// 基类
class Base{
greet(){
console.log('hello world')
}
}
// 派生类
class Derived extends Base {
greet(name:string){
console.log(`Hello, ${name.toUpperCase()}`)
}
}
// 错误:
/*
类型“Derived”中的属性“greet”不可分配给基类型“Base”中的同一属性。
不能将类型“(name: string) => void”分配给类型“() => void”
*/
示例中: 基类Base
中greet 方法是没有参数的, 但是派生类Derived中greet
方法的参数是必传参数,
此时TypeScript就会发出错误, 提示派生类中的属性greet
不可分配给基类Base
中的同一属性
如果此时还将派生类的实例赋值给使用基类类型注释的变量, 也将会发出错误
const b: Base = new Derived();
/*
错误:
不能将类型“Derived”分配给类型“Base”。
属性“greet”的类型不兼容。
不能将类型“(name: string) => void”分配给类型“() => void”
*/
示例中报错, 因为派生类和基类中的greet
属性不兼容
3.2 仅类型字段声明
当target >= ES2022
或者useDefinedForClassFields
这是为ture 时, 类字段在父类构造函数完成后初始化
覆盖父类设置的任何值, 当你只想为继承的字段重新声明更为精确的类型时, 这可能会成为问题
为了处理这种情况, 你可以使用declare
向TypeScript表明这个字段声明不应该有运行时影响
例如:
// 接口
interface Animal{
dataOfBirth:any
}
// 接口扩展
interface Dog extends Animal{
breed: any
}
// 基类
class AnimalHouse{
resident: Animal;
constructor(animal:Animal){
this.resident = animal
}
}
// 派生类
class DogHouse extends AnimalHouse{
// 不会对JavaScript 代码运行有任何影响
// 只是让属性的类型更加精准
declare resident:Dog;
constructor(dog: Dog){
super(dog)
}
}
3.3 初始化顺序
在某些情况下, JavaScript类的初始化顺序可能会令人惊讶,
例如:
// 基类
class Base{
name = 'base'
constructor(){
console.log('My name is '+ this.name)
}
}
// 派生类
class Derived extends Base{
name = 'derived'
}
const d = new Derived()
// 打印: My name is base
示例中答应name这是base
, 而不是derived
那这里发生了什么?
JavaScript定义的类初始化顺序是:
- 基类字段被初始化
name = 'base'
- 基类构造函数运行,
Base中constructor 执行
- 派生类字段被初始化:
name = 'derived'
- 派生构造函数运行
这意味着基类构造函数name
在其自己的构造函数中看到了自己的值, 因为此时派生类字段初始化尚未运行
4. 类成员可见性
你可以使用TypeScript来控制某些方法或属性是否对类外部的代码可见
4.1 public 公共
类成员默认可见性是public
, 可以在任何地方访问成员
例如
// 类
class Greeter{
public greet(){
console.log('hello')
}
}
// 实例
const g = new Greeter()
g.greet()
因为public
可见性修饰符 已经是默认的, 所以你不需要在 类的成员上编写此修饰符, 但有时可能出于样式/可读性的原因肯能会选择添加
4.2 protected 受保护的
protected
成员仅对声明它们的类以及当前类的子类可见
// 类
class Greeter{
// 公共的
public greet(){
console.log('hello, ' + this.getName())
}
// 受保护的
protected getName(){
return 'jack'
}
}
// 派生类(子类)
class SpecialGreeter extends Greeter{
public howdy(){
console.log('howdy, ' + this.getName())
}
}
// 基类的实例
const g = new Greeter()
g.greet()
g.getName()
// 错误: 属性“getName”受保护,只能在类“Greeter”及其子类中访问
// 派生类(子类)实例
const s = new SpecialGreeter()
s.howdy()
s.getName()
// 错误: 属性“getName”受保护,只能在类“Greeter”及其子类中访问
通过示例了解protected
修饰的属性和方法,只能在类内部使用, 不能再类的实例上调用
protected 成员曝光
派生类需要遵循其基类的契约, 但可以选择公开具有更过功能的的基类子类型, 这包括是protected
成员public
化
例如:
// 基类
class Base{
protected num = 10
}
// 派生类
class Dervied extends Base{
// 没有任何修饰符, 当前num就是public
num = 50
}
const d = new Dervied()
console.log(d.num) // 50
请注意, Dervied
类中的num
属性已经可以自由读写, 因此这要要注意, protected
修饰的属性主要在派生类中,如果没有添加修饰, 当前同名属性将变为public
, 如果这种暴露不是 故意的, 那么我们就需要小心重复修饰符
<>
4.3 private 私有的
private 就像protected
, 但不允许子类访问该成员, 只能在当前类中访问
例如:
// 基类
class Base{
private num = 10
}
// 派生类
class Dervied extends Base{
showNum(){
console.log(this.num)
// 属性“num”为私有属性,只能在类“Base”中访问。
}
}
// 基类实例化
const b = new Base()
console.log(b.num)
// 属性“num”为私有属性,只能在类“Base”中访问
// 派生类实例化
const d = new Dervied()
d.showNum()
示例中,发现private
修饰的属性为私有属性, 只能在当前类中访问, 和protected
很像
protected
修饰符的属性为受保护的属性,只能在类中访问,可以是当前类也是派生类
因为private 成员对派生类不可见, 所以派生类不能增加其可见性(否则报错)
// 基类
class Base{
private num = 10
showNum(){
console.log(this.num)
}
}
// 派生类
class Dervied extends Base{
num = 20
}
/*
类“Dervied”错误扩展基类“Base”。
属性“num”在类型“Base”中是私有属性,但在类型“Dervied”中不是
*/
跨实例访问private 成员
TypeScript允许跨实例访问private
成员
class A{
private x = 10;
sameAs(other:A){
return other.x === this.x
}
}
// 实例一
const a = new A()
// 实例二
const b = new A()
const bol = b.sameAs(a)
console.log('bol',bol)
// bol true
注意事项:
与TypeScript类型系统的其他方面一样,private
,protected
只在类型检查期间强制执行
这意味着像in
或者简单的JavaScript运行时构造的属性查找任然可以访问private
, protected
成员
// 类
class MySafe{
private num = 123
}
// 实例
const s = new MySafe()
console.log(s.num)
// TypeScript报错: 属性“num”为私有属性,只能在类“MySafe”中访问。
// 编译后, JavaScript运行, 依然可以答应出 123
private
还允许在类型检查期间使用括号表示法进行访问。这使得private
-declared 字段可能更容易访问单元测试等内容,缺点是这些字段是软私有的并且不严格执行隐私。
class MySafe{
private num = 123
}
const s = new MySafe()
// 通过中括号访问不报错
console.log(s['num']) // 123
与TypeScript的private
修饰符不同, JavaScript的私有字段(#
) 在编译后仍然是私有的, 并且不提供前面提到的转移舱口(如括号符号访问), 这使得他们成为了硬私有的
class Person {
#num = 10;
name = '张三';
constructor(){ }
}
// 实例化
const student = new Person()
console.log(student['#num']) // undefined 获取不到值
如果您需要保护类中的值免受恶意行为者的侵害,您应该使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。
5. 静态成员
类可能有static
成员, 这些成员不与类的特定实例相关联, 他们可以通过类构造函数对象本身访问:
例如:
// 类
class MyClass{
static num = 10;
static printNum(){
console.log(MyClass.num)
}
}
// 通过类名访问static静态成员
console.log(MyClass.num)
MyClass.printNum()
静态成员也可以使用相同的public
, protected
, 以及private
修饰符
// 类
class MyClass{
// 静态属性num 使用 了私有private 修饰符
// 此时num 只能在MyClass类中访问
private static num = 10;
static printNum(){
// ok
console.log(MyClass.num)
}
}
console.log(MyClass.num)
// 错误: 属性“num”为私有属性,只能在类“MyClass”中访问。
MyClass.printNum()
静态成员也会被继承
// 基类
class Base{
static getGreeting(){
return 'hello world'
}
}
// 派生类
class Derived extends Base{
myGreeting = Derived.getGreeting()
}
5.1 特殊静态名称
Function
从原型覆盖属性通常是不安全/不可能的, 因为类本省就是可以调用的函数. 所以不能将诸如类的名称, name, length, 和类函数属性call
定义为static
成员
class Person{
static name = 'hello'
// 静态属性“name”与构造函数“Person”的内置属性函数“name”冲突
}
6.类中的 static 块(代码块)
静态块允许你编写具有自己范围的语句序列,这些语句可以访问包含类中的私有字段, 这意味着我们可以编写具有编写语句的所有功能的初始代码, 不会泄露变量, 并且可以完全访问我们类的内部结构
例如:
// 类型
class Foo{
static #count = 0
get count(){
return Foo.#count;
}
static {
// 静态快
try {
const num = Math.floor(Math.random() * 10 )
Foo.#count += num
}catch(error){
}
}
}
7. 泛型类
类, 很像接口, 可以是泛型的,当使用实例化泛型类时, 其类型参数的推断方式与函数调用中方式相同
例如:
// 泛型类
class Box{
contents: Type
constructor(value: Type){
this.contents = value
}
}
// 实例化
const b = new Box("hello")
// const b: Box
console.log('b',b)
类可以像接口一样使用通用约束和默认值
7.1 静态成员中的类型参数
例:
// 泛型类
class Box{
static defaultValue: Type
// 错误: 静态成员不能引用类类型参数。
}
示例中的代码不合法, 原因在于类型总是会被完全擦除的
在运行时,只有一个Box.defaultValue
属性槽, 这意味着设置Box
(如果可能的话)也会被改变为Box
, 这样就很不好,
泛型类的static
成员 永远不能引用类的类型参数
8. 参数属性
TypeScript提供了特殊的语法,用于将构造函数参数转换为相同的名称和值的类属性, 这些称谓参数属性
是通过在构造函数参数前面加上修饰符public
,protected
, private
或readonly
.来创建,
没有参数属性前,如果想让参数作为属性, 需要赋值处理
例如:
class Params {
x: number;
y: number;
// 构造函数
constructor(x:number,y:number){
this.x = x;
this.y = y;
}
}
// 实例化
const a = new Params(10,20)
console.log(a) // {}
现在通过在参数前添加修饰符, 参数直接可以变成属性
例如:
// 类
class Params {
constructor(
public readonly x :number ,
protected y:number ,
private z:number
){
// ...
}
}
const a = new Params(1,2,3)
console.log(a)
/*
添加完修饰符,编译后运行, 浏览器打印的a对象上就有x,y,z三个参数属性
{x: 1, y: 2, z: 3}
*/
// 1.x 属性为公共只读属性
console.log(a.x)
// (property) Params.x: number
a.x = 20
// 错误:无法分配到 "x" ,因为它是只读属性
// 2. y 属性为受保护属性, 实例上无法获取
console.log(a.y)
// 错误:属性“y”受保护,只能在类“Params”及其子类中访问
// 3. z属性为私有的,只有在当前类中可以访问
console.log(a.z)
// 错误:属性“z”为私有属性,只能在类“Params”中访问。
10.类的表达式
类的表达式与类声明非常相似, 唯一真正的区别是类的表达式不需要定义名称, 我们可以通过他们最终绑定到的任何标识符来引用它们
例如:
// 类的表达式
const SomeClass = class {
content:Type;
constructor(value:Type){
this.content = value
}
}
// 实例化
const m = new SomeClass("hello world")
// const m: SomeClass
11 abstract
抽象类和成员
11.1 抽象类和抽象成员
TypeScript中的类, 方法, 和字段可能是抽象的
抽象方法或抽象字段是尚未提供实现的方法, 有点像重载,只定义解构类型,
这些抽象成员必须存在于抽象类中, 不能直接实例化
抽象类的作用是作为实现所有抽象成员的子类的基类, 但一个类没有任何抽象成员时, 就说他是具体的类
例子:
// 抽象类
abstract class Base{
// 抽象方法
abstract getName():string
printName(){
console.log('Hello '+ this.getName())
}
}
// 实例化抽象类
const b = new Base()
// 报错: 无法创建抽象类的实例
我们无法实例化Base
, 因为他是抽象类, 相反,我们需要创建一个派生类并实现抽象成员
// 抽象类
abstract class Base{
name: string
constructor(name:string){
this.name = name
}
// 抽象方法
abstract getName():string
printName(){
console.log('Hello '+ this.getName())
}
}
// 派生类: 实现抽象成员
class Student extends Base{
age: number
constructor(name:string,age:number){
super(name)
this.age = age
}
// 抽象成员的实现
getName(): string {
return this.name
}
}
// 实例化派生类(实现类)
const b = new Student('小明',18)
b.printName()
请注意, 如果我们忘记实现基类(抽象类)的抽象成员, 我们会得到一个错误
// 派生类: 实现抽象成员
class Student extends Base{
// 错误: 非抽象类“Derived”不会实现继承自“Base”类的抽象成员“getName”。
}
抽象类做为其他派生的基类使用,他们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节
abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法
11.2. 抽象构造签名
有时你想接受一些类构造函数, 它产生一个派生自某个抽象类的类的实例
例如:
// 抽象类
abstract class Base{
// 抽象方法
abstract getName():string
printName(){
console.log('Hello '+ this.getName())
}
}
function greet(ctor: typeof Base){
const instance = new ctor()
// 报错: 无法创建抽象类的实例。
instance.printName()
}
Typescript 正确的告诉您, 您正在尝试实例化一个抽象类,
相反, 你想编写一个接受带有构造函数签名函数
function greet(ctor: new () => Base){
const instance = new ctor()
instance.printName()
}
greet(Derived)
greet(Base)
/*
报错:
类型“typeof Base”的参数不能赋给类型“new () => Base”的参数。
无法将抽象构造函数类型分配给非抽象构造函数类型。
*/
现在 TypeScript 正确地告诉您可以调用哪些类构造函数 -Derived
可以,因为它是具体的,但Base
不能。
12. 类之间的关系
在大多数情况下, TypeScript中的类在结构上进行比较, 与其他类型相同
例如, 如下两个类可以相互替代使用, 因为他们是相同的
// 类1
class Point1{
x = 0;
y = 0;
}
// 类2
class Point2{
x = 0;
y = 0;
}
// OK
const p:Point1 = new Point2()
因为两个类相同, 所有可以将Point2
类的实例化对象赋值给使用Point1
类作为类型注释的变量
同样, 即使没有显示继承, 类之间的子类型关系也是存在
// 隐式的子类
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){
console.log(x)
}
// 以前全部ok
fn(window)
fn({})
fn(fn)
13. 类的其他用法
类除了可是实现接口外, 接口也可以反过来扩展类,
简单说就是可以把类当作接口使用
例如:
// 把类当成接口使用(相当于一个接口)
class Person{
name:string
age: number
}
interface Student extends Person{
sex: string
}
let xiaoming:Student = {name:"小明", age: 18, sex: "男"}
console.log('xiaoming', xiaoming)