前言
本文收录于TypeScript知识总结系列文章,欢迎指正!
在早期的JavaScript中,我们使用函数来模拟类,使用原型链实现继承行为。在ES6以后JS也引入类的概念,虽然其本质还是使用原型继承和构造函数的语法糖实现的,但是类的写法依旧使得开发人员得心应手;相比JS的类,TS的类可以说是有过之无不及。
在TypeScript中,类可以包含属性和方法,支持继承和接口实现,也可以使用修饰符来控制访问权限。类的定义使用class关键字,并可以在其中声明构造函数、属性、方法等。
基本用法
还记得之前的接口吗?我们把要实现的对象或者类通过接口的形式抽象表现出来,然后通过实现类或者绑定对象将接口具象化
沿用之前的例子展示一下类的基本写法,其中name是类成员(属性),constructor是构造函数,getName是成员方法
class Animal { name: string constructor(name: string) { this.name = name } getName() { return this.name } } const animal = new Animal("dog") console.log(animal.getName());
实现接口
在接口中我们说到了类可以用了实现接口或者是对象类型别名,下面是一个例子,我们复习一下
interface IAnimel { name: string likeMeat: boolean getName(): string } class Animal implements IAnimel { name: string likeMeat: boolean constructor(name: string, likeMeat: boolean) { this.name = name this.likeMeat = likeMeat } getName() { return this.name } } const animal = new Animal("dog", true) console.log(animal.getName());
继承(extends)
在TS中类的继承是通过extends关键字来实现的。类可以继承其他类的属性和方法,被继承的类称为父类或基类,继承的类称为子类或派生类;在之前的文章中,我详细的讲述了JS的继承实现及使用,有兴趣可以看看
基本用法
下面是一个继承的基本写法
class Animal { name: string constructor(name: string) { this.name = name } getName() { return this.name } } class Dog extends Animal { constructor(name: string) { super(name) } } console.log(new Dog("阿黄").getName());
可以看到,Dog类继承于Animal类,并拥有Animal的属性及方法
访问父类
在TS中,子类中可以通过super来访问父类的属性和方法。
例如,如果父类有一个属性name,可以在子类中通过super.name来访问该属性。同时,如果子类中有与父类同名的属性或方法,可以使用super关键字来调用父类的属性或方法,如super.getName()
class Animal { name: string constructor(name: string) { this.name = name; } getName() { return this.name } } class Dog extends Animal { name: string = null constructor(name: string) { super(name); this.name = "小黑" console.log(this.name, super.name);// 小黑 undefined } getAllName(): string { return this.name + super.getName()// 获取了当前类的name } } console.log(new Dog("阿黄").getAllName());// 小黑小黑
然而在TS编译成JS时,会出现取不到super.name的情况,详细解释可以参考这篇文章
但实际上TS识别出了super关键字取得是父类的name,JS无法实现
如果需要访问super.name,可以使用修改构造函数原型的方式实现效果
Animal.prototype.name = "阿黄" console.log(new Dog("阿黄").getAllName());// 小黑阿黄
重写父类(override)
重写父类是面向对象常用的技术之一,它的定义是在子类中重新定义父类原有方法。重写父类的方法或属性可以修改父类行为,实现多态性;重写可以(也可以不使用,函数名相同即可重写)使用override关键字显式标注需要重写的方法,这样可以提高代码可读性和可维护性
下面是一个重写的案例,如果子类没有重写父类的方法,就会报错
class Animal { name: string constructor(name: string) { this.name = name; } getName() { throw new Error("记得重写我") } } class Dog extends Animal { override getName(): string { return this.name } } console.log(new Dog("阿黄").getName());// 阿黄
除此之外,子类可以在重写的方法中调用父类的方法,达到拓展的作用
class Animal { name: string constructor(name: string) { this.name = name; } getName() { return this.name } } class Dog extends Animal { override getName(): string { const name = super.getName() return "名字叫" + name } } console.log(new Dog("阿黄").getName());// 名字叫阿黄
只读关键字(readonly)
在接口中我们提到了readonly(只读属性),这种写法在类中同样适用
class Animal { readonly name: string constructor(name: string) { this.name = name; } setName(name: string) { this.name = name// 抛错:无法分配到 "name" ,因为它是只读属性 } } console.log(new Animal("阿黄").setName("小黑"));
上述代码我们可以看到,使用了只读关键字的属性只能在构造函数中赋值
tips:只读属性只能在赋值时进行拦截,功能与set比较像,它无法拦截对象的操作
type Arr = string[] class Animal { readonly color: Arr constructor(color: Arr) { this.color = color; } setName(name: string) { this.color.push(name) return this.color } } console.log(new Animal(["阿黄"]).setName("小黑"));// [ '阿黄', '小黑' ]
存取器(getters/setters)
在之前文章中我介绍过JS对象属性的存取,使用Object.defineProperty对对象进行读写。在ES6的类中同样引入了这么一个定义属性的方式:类存取器,它是一种特殊类型的函数,用于获取和设置类属性的值;它使用get和set关键字定义
class Animal { _name: string set name(val: string) { console.log("设置了值"); this._name = val } get name() { console.log("获取了值"); return this._name } } const animal = new Animal() animal.name = "阿黄"// 设置了值 console.log(animal.name);// 获取了值 阿黄
静态成员(static)
类的静态成员也是在JS中就已经实现了的功能,它是指通过类本身直接访问,而不是通过类的实例访问的成员。它们可以是静态属性或静态方法,它的作用是在不需要创建类的实例的情况下访问类的成员,如我们可以通过实现一个单例来举个例子
class Animal { name: string static __ins: Animal constructor(name: string) { this.name = name } static instance() { return (name: string) => { !!!this.__ins && (this.__ins = new Animal(name)) return this.__ins } } } const animal1 = Animal.instance()("阿黄").name const animal2 = Animal.instance()("小黑").name console.log(animal1, animal2, Animal.__ins);// 阿黄 阿黄 Animal
上面代码中,我定义了一个static属性 __ins 以及一个static方法 instance ,在使用时直接使用类名.属性获取,此时如果使用下面的代码无法获取静态属性
new Animal("阿黄").__ins
访问修饰符(类成员属性)
封装是面向对象编程的技术之一,在代码中可以通过控制属性和方法的可访问性,达到封装的目的
JavaScript中的封装
在早期的JS中我们通常使用闭包的写法来实现函数私有变量的使用及储存,并且习惯使用 _变量名的形式对私有变量进行标记
var Animal = (function () { var _name = null; function Animal(name) { _name = name; } Animal.prototype = { getName() { return _name; }, }; return Animal; })();
到了ES11(2020年)针对JS类的内部私有化的功能也被实现了,使用 #变量名 的方式将属性标记为私有变量
class Animal { #name; getName() { return this.#name; } setName(name) { this.#name = name; } } const animal = new Animal(); animal.setName("阿黄"); console.log(animal.getName());
TypeScript中的封装
TS提供三个访问修饰符:public、private、protected
公共(public)
类中的属性和方法默认使用的就是公共(public)修饰符,表示该成员可以在类的内部和外部被访问
class Animal { public name: string; constructor(name: string) { this.name = name } public getName() { return this.name; } } const animal = new Animal("阿黄") console.log(animal.getName(), animal.name);// 阿黄 阿黄
私有(private)
私有(private)用于限制类成员只能在类内部访问,不能在类外部访问(子类也无法访问属性及方法)
class Animal { private name: string; constructor(name: string) { this.name = name } private getName() { return this.name;// 正常访问 } } const animal = new Animal("阿黄") console.log(animal.getName(), animal.name);// 抛错:属性为私有属性,只能在类中访问
结合之前说的存取器,我们可以实现一个完整的TS类存取器
class Animal { private _name: string; get name() { return this._name; } set name(val: string) { this._name = val } }
受保护(protected)
如果给属性或者方法定义为受保护(protected)的,说明该属性或方法只能在类内部或派生类(子类)中访问
class Animal { protected _name: string; constructor(name: string) { this._name = name } } class Dog extends Animal { getName = () => { return this._name } } const dog = new Dog("阿黄") console.log(dog.getName());// 阿黄 console.log(dog._name);// 抛错:属性受保护,只能在类及其子类中访问
参数的修饰符
结合上述的修饰符,我们还可以在类的构造函数的参数中直接赋予修饰符达到简写的目的,例如
class Animal { constructor(readonly name: string, private likeMeat?: boolean, public age?: number, protected color?: string) { } } const animal = new Animal("阿黄", true, 10, "black") console.log(animal); // Animal { name: '阿黄', likeMeat: true, age: 10, color: 'black' }
上述代码的效果和以下代码效果相同
class Animal { readonly name: string private likeMeat?: boolean public age?: number protected color?: string constructor(name: string, likeMeat?: boolean, age?: number, color?: string) { this.name = name this.likeMeat = likeMeat this.age = age this.color = color } } const animal = new Animal("阿黄", true, 10, "black") console.log(animal); // Animal { name: '阿黄', likeMeat: true, age: 10, color: 'black' }
抽象类和抽象方法(abstract)
同一个抽象类的不同子类可以实现相同的抽象方法,但具体的实现方式不同,这是多态的实现。
抽象类和抽象方法是多态的基础。在TS中抽象类是一种特殊的类,不能被直接实例化,只能被子类继承,抽象类中的抽象方法必须在子类中被实现,通过强制子类实现指定的属性或方法,可以保证子类在一定程度上的一致性
我们来看一个例子
abstract class Animal { constructor(protected name: string) { } abstract getName(): string } class Dog extends Animal { getName(): string { return this.name } } const animal = new Dog("阿黄") console.log(animal.getName())
例子中我使用定义了一个抽象类Animal,并且定义了getName函数,此时的类无法直接实例化,因为getName没有被实现,需要使用其他类(Dog类)继承基类并实现这个函数,最后实例化子类,如果子类也是抽象类,那么则继续被继承,直到抽象类中的抽象函数全部被实现为止
接口(interface),抽象(abstract),重写(override)
从概念上来说这三者都是实现多态的方式,它们三者的差异体现在约束的方式和约束的程度上
重写主要是针对方法的实现进行约束:遵循父类方法的参数和返回值类型
接口主要是针对行为进行约束:类需要实现哪些方法以及这些方法应该具有什么样的参数和返回值
抽象则同时约束行为的实现并提供部分方法实现:子类可以直接继承并使用父类提供的方法或属性,并且被父类约束方法的签名和返回值类型
结语
本篇文章详细的介绍了TypeScript中类的定义及使用,使用类实现接口,类的继承、封装、多态,
只读属性,存取器,静态属性并且结合接口总结了多态的表现方式
最后,感谢你的阅读,有任何问题欢迎评论或私信,如果文章对你有帮助,还希望支持一下博主!