类 class
作为类型存在,类似于接口
作为值存在,类似于构造函数
类按照继承特性来说存在父类(基类);子类(派生类)
基类通常为抽象类,抽象一系列通用方法属性,具体实现由子类确定
类的结构
- 静态属性和原型方法
- 静态属性是具备static修饰符的属性,只能由类本身调用
- 原型方法是不具备static修饰符的所有类中定义的方法,可以被实例和类本身调用
- 静态部分和实例部分
- 静态部分:默认存在于class中的构造器
- 实例部分:在方法中用
this指代的属性,或者class内定义的不带有static修饰符的属性及原型方法
- 作为值存在,静态部分(因为至少存在构造器)必须具备
- 作为类型存在,只会将实例部分作为类型结构
class TestClass {
x: string; //实例对象属性,es2022新写法,所有实例对象属性可以放在非constructor中直接定义
constructor(x:string) { //必然会有constructor构造器函数,没有显式定义会默认存在空constructor
this.x = x
}
toString() { //和constructor一样定义在类原型对象上,所有实例对象共享的原型方法
}
}
// 上述写法对应构造函数写法
function TestClass(x){
this.x = x
}
TestClass.prototype = {
constructor() {},
toString() {}
};
let testclass1 = new TestClass('a');
console.log(testclass1.hasOwnProperty('x')); //true ,属于实例属性
console.log(testclass1.hasOwnProperty('toString')); //false 实例对象本身不具备该属性
类的封装,继承,多态以及实现
封装:利用类成员修饰符
不加修饰符默认为public
对于不想暴露给外部又希望实现的方法属性利用private修饰符进行封装
仅针对于TS编写,编译过程中进行校验规范的封装特性,实际编译成JS以后的成员没有此限制,均可访问
- public:公共属性
- 可被自身调用
- 可被派生类调用
- 可被实例调用
- private: 私有属性
- 仅可被自身调用
- 类似于构造函数中的变量赋值语句
- protected
- 可被自身调用
- 可被派生类调用
class Father { name: string; constructor(name: string) { this.name = name; } protected say(word: string = 'nothing') { console.log(word); } } class Son extends Father { constructor(name: string) { super(name) } private talk(word: string = 'anything') { console.log(word); } public testTalk(outWord: string) { this.talk(outWord) } say(word: string = 'anything') { super.say(word) //父类protected 子类可调用 } } let father = new Father('rock'); // father.say('hello'); //父类protected,实例不可调用 let son = new Son('nicky'); // son.talk() //私有属性,实例不可调用 son.testTalk('outsideThing') //outsideThing
继承:
- 继承具备从属关系,一个类只能继承一个基础父类
class Apartment extends Building {}
- 子类继承自父类,具备父类的实例属性,原型方法
- 继承只关注可访问性修饰符。当父类成员的可访问性为
protected和public时,不论其是否为静态或只读属性都可被继承
实现:
- 实现无从属关系,可以有多个实现
class Apartment implements Price, Area, ZoneSize {}
- 类可以实现 抽象类的抽象属性方法
- 类可以实现 接口
多态:不同子类和父类的同名属性可以有不同实现
//父类;基类
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
//子类;派生类
class Snake extends Animal {
constructor(name: string) { super(name); };
move(){
console.log('just want to climb')
}
}
class Horse extends Animal {
constructor(name: string) {super(name)};
move(dis = 10) {
super.move(dis) //等价于Animal.move(dir)
}
}
注意:
- 类的多态性使得子类可以有各自的实现,但子类继承自父类的方法在封装方式(修饰符的使用)上必须保持一致
- 父类constructor用
private封装以后,不可被实例,也无法被继承 - 父类constructor用
protected封装以后,不可被实例,只能被继承 - 当类作为类型使用时,其实例属性只能被
public封装,只要存在非公共的实例成员,此类不可作为类型使用,因为私有属性不可被外部访问,因此也无法正常赋值
类修饰符
修饰符仅用在成员属性前
静态修饰符: static
- TS和ES6中都支持的修饰符,是JS运行时修饰符
- 表示属性为 内置属性 仅可被类自身调用,不是实例属性,也不会作为类型结构部分
- 对于使用
static修饰的属性方法,类更像是一个命名空间
可访问性修饰符: public, private, protected
- 此修饰符优先级最高,要优先保证属性的可访问性,才能设定是否只读
- 继承
public和protected的子类都可实现多态,但protected确保子类 多态 的同时,不会暴露属性
读写性修饰符: readonly
此修饰符表示属性只可读,仅能在类声明过程中对属性赋值,实例对象不可修改该属性
修饰符特别用法:
- 配合方法传参可以简化属性的定义方式
class SimplifyPro { constructor(readonly name: string) { } } //等效于 class SimplifyPro { readonly name: string; constructor(name: string) { this.name = name } }
类存取器
- 适用于ES5之后的语法,ES3不支持
- 存取器由getter取值/setter存值函数构成
- 目的:利用存取器封装一系列监听拦截的方法对内置(
private修饰),受保护(protected修饰)或静态属性(static修饰) 进行有限制的对外暴露
/** 私有属性的存取器 */
class Person {
constructor(private _name: string) {}
get yourName():string {
return this._name //私有属性不能被访问,但通过get取值函数可以获得
}
set yourName(name: string) { //不能具有返回值的类型批注---因为修改值并不确定类型
this._name = name
}
}
let jake = new Person('jake');
// jake._name //报错,私有属性不能被实例对象访问
console.log(jake.yourName); //jake
jake.yourName = 'Bob';
console.log(jake.yourName); //Bob
/** 静态属性的存取器 */
class StaticClass0 {
static myName: string = 'myName';
get staticName() {
return StaticClass0.myName
}
set staticName(name) {
StaticClass0.myName = 'outer-' + name
}
}
// 1.实例调用
let staticClass0 = new StaticClass0();
console.log(staticClass0.staticName); //myName
//2.type
let staticType1: StaticClass0 = {
// myName: '1', //error:不能有静态属性,对象字面量赋值时类的静态部分不会作为类型约束
get staticName() {
return StaticClass0.myName
},
set staticName(name) {
StaticClass0.myName = name
}
}
抽象类 & 抽象属性/方法
可用抽象类表述某个实例的形状, 再用子类实例化 通常抽象类用于定义类型,派生类用于作为构造函数实现具体功能
定义及使用
-
抽象类&抽象方法/属性 作用:当强制每个派生类都具备某种功能方法,通用父类又无法提供各具特性的实现,就需要 抽象类 进行 功能的抽象(抽象方法),从而引导子类进行具体实现功能
-
可以理解 抽象 就是 强制 派生类必须有具体实现 的指令
-
abstract关键字声明的类为抽象类 -
抽象类中使用
abstract关键字声明的 属性/方法 为 抽象属性/方法 -
抽象类只是一些公共属性和方法的集合,本质无具体实现,因此无法创建实例,但仍可以作为类型结构使用
-
只有抽象类中才可存在抽象方法
abstract class Menu0 { //抽象方法 abstract setName(name: string): void; //原型方法 arrayGetName: () => string; getName():string { return 'menu0' } } class FruitMenu0 extends Menu0 { setName(name: string): void { //必须有setName的具体实现,而非抽象方法可以不用重新定义 console.log('fruit0'); } } let menu0: Menu0 = { //ok setName() {}, getName() {return 'menu--instance'}, arrayGetName() {return 'instance'} }; -
抽象类不可被实例,但仍然存在构造器,可以被子类继承并super调用
abstract class Class1 { constructor(a:number) { console.log(a); } } class Class2 extends Class1 { b:number; constructor(a: number, b:number) { super(a); //继承父类后可以使用其构造方法 console.log(b); } } let class1 = new Class2(1,2);
子类extends和implement 抽象类 的区别
- 首先明确 抽象类 中 不一定 只有抽象方法,也可以有 具体实现的方法属性
- 因此 子类 实现和继承的区别 主要是 对于抽象和非抽象的方法属性的区别:
使用extends,表示 子类 仍然是 继承自 父类,父类的非抽象方法不用具体实现
使用implement就需要 子类 实现所有父类抽象或非抽象的方法
扩展说明
设计模式中的 单例模式 和 类的结合
单例模式存在价值
- 某个类只有唯一实例
- 全局可调用此 类的实例,但无法生成或任意修改此实例
- 保证数据的唯一性,如保存全局使用的数据库信息
单例模式创建方式
- 私有构造器——防止对外调用生成新的实例
- 静态生成实例方法——拦截并选择性 调用私有构造器生成唯一实例 或 获取缓存的唯一实例
- 静态实例缓存——用于缓存唯一实例,不可被外部访问
class Ones {
private static instance: Ones;
private constructor(n: string) {}
static getInstance(name: string) {
if (!this.instance) this.instance = new Ones();
return this.instance;
}
}
其他
- 类中默认存在
constructor。类作为类型结构 和 接口 的区别 还在于 能通过构造方式赋值实例属性,所以类型定义时存在如下情况://就算不写constructor,类默认有一个不接收传参的constructor,因此如下写法error class Mod3 { //error exmname: string; specname: string; } //由上,因缺乏构造器接收传参赋值,结构定义也没有给实例属性初始值,而类具备构造函数使用的方式可以改写为如下格式: class Mod3 { exmname: string | undefined; specname: string | undefined; //or exmname = 'initial'; //类型推断为string specname = 'initial'; } - 类如果采用修饰符传参方式定义成员属性,在构造函数中成员属性赋值优先于方法调用
class Person { logName() { console.log(this.name); } constructor(readonly name : string) { //name的获取优先于调用logName this.logName() } } let person = new Person('jake'); //jake - 在tsconfig.json中将
target设置为es5以下的版本,如es3,存取器的写法会报错 - 作为es5语法的存取器不仅仅类中可用,使用
Object.defineProperty设置一般属性的setter和getter后,此属性即为可存取属性,这种属性在读写的时候会调用get/set方法,因此可以对写入进行一定的条件限制 - 构造签名,构造函数类型等区别说明:用构造签名的方式可组成构造函数类型,拥有构造函数类型的函数为构造函数,此函数可用于创建实例对象
- 构造签名 → 构造函数类型 → 构造函数 → 实例对象
- 其中的 构造函数类型 可以由接口或类型声明的方式定义
- 而 构造函数 可以由class定义
- 因为类的静态部分constructor不是类型声明,无法作为类型被调用或检查, 因此需要借助 由接口或类型声明定义的 构造函数类型 以及 实例部分类型 封装的工厂函数来校验 构造函数 的构造器是否符合要求
//实例部分类型 interface Point { x: number; y: number; } //构造函数类型 interface PointConstructor { new (x: number, y: number): Point; } //类定义的一个构造函数结构(包含实现) class Point2D implements Point { readonly x: number; readonly y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } //一个具体实现的构造函数方法 ----工厂函数 function newPoint( pointConstructor: PointConstructor, x: number, y: number ): Point { return new pointConstructor(x, y); } //使用了复合构造函数类型的class定义的构造函数结构创建的一个实例对象 const point: Point = newPoint(Point2D, 2, 2);
遗留疑问
- constructor和其他方法一样在原型上,可被实例调用?为什么在类作为类型的时候,constructor不会作为类型的一部分?