写下博客主要用来分享知识内容,并便于自我复习和总结。
如有错误之处,请各位大佬指出。
类的定义
为了方便理解,我们先对比一下在es5中很像是类的用法:(构造函数)
// 构造函数
function Person(){
this.name = "ls" // 属性
this.run = function(){ // 实例方法
console.log(this.name + "在跑步")
}
}
Person.getInfo = function(){
console.log("我是静态方法")
}
// 原型链上的属性会被多个实例共享
Person.prototype.sex = "男"
Person.prototype.work = function(){
console.log(this.name + "在工作")
}
let p = new Person()
console.log(p.name)
p.run()
console.log(p.sex)
p.work()
Person.getInfo()
再来看一下java的类:
public class ObjectAndClass {
public static void main(String args[]) {
pet cat = new pet("猫"); // 可以给构造函数传递参数
cat.say(); // 调用方法
System.out.println(cat.name); // 可以获取
pet dog = new pet(); // 也可以不传递参数
dog.say();
}
}
// 类,简单来说就是存储对象的属性和方法
class pet {
// 对象标识符在java里分为private、public、default、protected,如果不标注默认为public
// 被public标注的是公有变量,我们可以在类外使用
// 被private标注的是私有变量,只能在类里使用
// 具体内容在这里就不说了,在下面还会提到
// 并且,在java里,数据类型是一定要指明的。
String name = "狗";
private int a = 1;
// java里的构造函数和类名是相同的
// 就算不手动创建,它其实也一定会创建这个无参的构造函数
// 那既然它会自动创建,那为什么要手动创建?
// 这里需要注意:
// 1、如果类里没有构造函数,那么不传参数也不会报错
// 因为它自动创建了无参的构造函数
// 2、但是如果类里存在有参的构造函数,
// 那么这个无参构造函数,如果不手动创建,它是没效果的
// 也就是说,这时不传参数是会报错的
// 3、所以这也就是为什么,虽然我们创建了有参的构造函数
// 习惯上还要创建无参的构造函数
pet() {}
// 手动创建了有参构造函数,之后传递参数时一定需要严格对照
// 这也就是之前提到的函数的重载
// 在java里的重载,同名函数只要形参不同,那么调用同名函数时,就会根据实参的不同来调用函数
// 此时,我们创建实例时,如果传递参数,就会赋值给name
// 如果不赋值,name就会为默认值
pet(String name) {
this.name = name;
}
public void say(){
System.out.println(this.name);
}
}
如果对上面的说明有了一些理解之后,我们来看一下ts中的类:
class Person{
name:string // 属性,前面省略了public关键词
// 注意:这里的构造函数和java一样,
// 它其实也会创建一个无参的构造函数,
// 但因为我们创建了有参的构造函数,
// 所以无法使用无参构造函数,即一定要传参
// 为什么一定要注意这部分问题,看下面的继承就知道了
constructor(name:string) { // 构造函数,实例化类的时候触发的方法
this.name = name
}
// 普通函数
run():void{
console.log(this.name + "在运动")
}
// set和get,获取和设置相应属性
getName():string{
return this.name
}
setName(name:string):void{
this.name = name
}
}
let p = new Person("李四")
p.run()
这里还有一些注意点。
刚才我们提到可以没有构造函数,它会默认创建一个无参构造函数,以下代码就可以证明:
class Person{
name:string = "李四"
run():string{
return `${this.name}在运动`
}
}
let p = new Person() // 可以不传参,这时类里的参数就会使用默认值
console.log(p.run())
但不能传参的用处肯定很少,我们不可能总用默认值的,所以我们需要创建有参构造函数。但这时就不能不传参数了。那它能不能像Java一样,创建很多构造函数呢?而且之前说到过ts存在重载机制的,如果手动创建一个无参构造函数不就解决问题了?
class Person{
name:string = "李四"
constructor() {}
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动`
}
}
通过代码报错,我们得知,这还真不行,constructor构造函数只能构建一个。
继承
也许你在学习js时,没有听过继承(因为我很感同身受,在学习js过程中,不知道js也有继承的方式。直到后来准备面试的时候,遇到继承问题)。因此,在说ts的继承前,如果各位不知道什么是继承,可以来学习一下:JS继承的几种方式
说回正题,在ts中实现继承的方式,和java依旧很像,在这里就不展示java的写法了,直接看ts。
class Person{
name:string
// 此时注意之前反复提到的构造函数问题
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动`
}
}
// extends:继承
// 继承后的子类,拥有父类的所有属性和方法
class Web extends Person{
}
// let w = new Web() // 不传参数会报错
// 此时可以发现,虽然子类没有任何内容,
// 但我们传递了参数,它依然输出内容
// 这也就说明了,此时传递参数,它会去找父类的构造函数,
// 而不是子类的无参构造函数
let w = new Web("李四")
console.log(w.run()) // 输出:李四在运动
在继承的时候,还有些问题需要注意:
假如子类不想用父类的构造函数,我们对其重写可以吗?
class Web extends Person{
name: string
age: number
constructor(name:string,age:number) {
this.name = name
this.age = age
}
}
结果来看,它直接报错:
也就是说,继承后的子类,构造函数里一定要用super。也就是说,一定要用到父类的构造函数。基本用法是这样的:
class Person{
name:string
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动`
}
}
class Web extends Person{
constructor(name:string) {
super(name)
}
}
let w = new Web("老六")
console.log(w.run())
那么这里有个问题,如果子类中有和父类相同的同名函数和属性,它会使用父类的还是子类的呢?
class Person{
name:string = "李四"
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动1`
}
}
class Web extends Person{
name: string = "王五"
constructor(name:string) {
super(name)
}
run():string{
return `${this.name}在运动2`
}
}
let w = new Web("老六")
console.log(w.run())
先从结果看,如果子类和父类拥有同名函数,它会去使用子类的函数。并且,子类中的this.name用的是子类中的name,而不是父类的,并且传递的参数没起到作用。这是因为,我们传递参数的时候,它会去调用构造函数,构造函数中用到了super,也就是去用父类的构造函数,将传递的值赋给父类。因此,传递的参数不会影响到子类。而子类中出现了同名参数name,所以父类同名参数被覆盖了。同时,因为其设置了默认参数,所以就展示出了这样的内容。
那我们的本意应该是传递参数,可以影响输出结果,所以可以这么做:
(又或者在子类中不去定义同名参数)
class Person{
name:string = "李四"
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动1`
}
}
class Web extends Person{
name: string = "王五"
constructor(name:string) {
super(name)
this.name = name // 在子类中作出改变
}
run():string{
return `${this.name}在运动2`
}
}
let w = new Web("老六")
console.log(w.run())
假如把子类的run函数去掉,那就输出:老六在运动1
(虽然在该例中,用super传递参数给父类没有用,但那是因为我们用了同名属性,它会被子类覆盖。一般情况下,继承的目的就是在父类的基础上添加新属性和新方法,或者改进旧方法,因此同名属性使用不当可能会造成误解噢,因为本来就可以共用同一个属性。总之,在面对继承问题时,需要小心同名覆盖问题)
综上所述,如果子类中没有构造函数,它就会自动去使用父类的构造函数。而子类中如果写构造函数,那么它一定要用super去调用父类的构造函数。调用结束后,继续执行子类构造函数中内容。除了构造函数,也就是说,如果子类和父类有同名函数和属性,那么父类的内容就会被子类覆盖。
对于super还有一点补充:super不是只能用于构造函数,其它函数也可以用:
class Person{
name:string = "李四"
getName():string{
return this.name
}
}
class Web extends Person{
run():string{
return super.getName()+'在跑步'
}
eat():string{
return super.getName()+'在吃饭'
}
}
let w = new Web()
console.log(w.run())
console.log(w.eat())
对于构造函数还有一点补充:我们在使用构造函数的时候,还有一种简化代码方式:
(这样做之后,我们就不需要额外声明属性,只需要加一个public关键字就可以了)
class Person{
constructor(public name:string) {}
getName():string{
return this.name
}
}
class Boy extends Person{
constructor(public age:number) {
super('李四')
}
getAge():number{
return this.age
}
}
const p = new Boy(15)
console.log(p.getName())
console.log(p.getAge())
类里面的修饰符
虽然之前在定义属性的时候没有写修饰符,但它们在ts中是存在的。
ts提供了三种修饰符:public、protected、private。
public:公有类型。在当前类里面、子类、当前类外面都可以访问。
(如果省略修饰符,那么属性的修饰符默认为public)
class Person{
public name:string // 公有属性
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动` // 公有属性在类里可以访问
}
}
class Web extends Person{
constructor(name:string) {
super(name)
}
work():string{
return `${this.name}在工作` // 公有属性在子类可以访问
}
}
let p = new Person("李四")
console.log(p.name) // 公有属性在类外可以访问
protected:保护类型。在当前类里面、子类里面可以访问,在当前类外面无法访问。
class Person{
protected name:string // 保护属性
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动` // 保护属性在类里可以访问
}
}
class Web extends Person{
constructor(name:string) {
super(name)
}
work():string{
return `${this.name}在工作` // 保护属性在子类可以访问
}
}
let p = new Person("李四")
// 报错:
console.log(p.name) // 保护属性在类外无法访问
private:私有类型。在当前类里面可以访问,子类、当前类外面都无法访问。
class Person{
private name:string // 私有属性
constructor(name:string) {
this.name = name
}
run():string{
return `${this.name}在运动` // 私有属性在类里可以访问
}
}
class Web extends Person{
constructor(name:string) {
super(name)
}
work():string{
// 报错:
return `${this.name}在工作` // 私有属性在子类无法访问
}
}
let p = new Person("李四")
// 报错:
console.log(p.name) // 私有属性在类外无法访问
知道了上述三种修饰符的特性后,再补充一些内容:
刚才的继承中,我们知道子类会覆盖父类中同名属性和方法,那修饰符可以覆盖吗?
class Person{
private age:number;
constructor(age:number) {
this.age = age;
}
}
class Web extends Person{
public age:number
constructor(age:number) {
super(age)
this.age = age;
}
}
let p = new Person(15)
结果表明,我们无法修改同名属性的修饰符:
那会不会是private的问题呢,因为private只能在当前类里访问?那我们交换上面代码的修饰符:
class Person{
public age:number;
constructor(age:number) {
this.age = age;
}
}
class Web extends Person{
private age:number
constructor(age:number) {
super(age)
this.age = age;
}
}
let p = new Person(15)
结果表明,我们依然无法修改。
其它的同理,只要涉及到修饰符的变化,都会报错。
道理其实很简单,我既然在父类中为属性或方法设置了相应修饰符,也就代表着我希望它们的作用范围被控制,这样得到的结果一定会是我们想要的。既然父类都设置好了属性或方法的修饰符,那就不希望它在相应的作用范围外被调用。现在在子类中却想修改它,只可能是误操作,所以ts报错(除非是设计父类时出现问题)。
那我想定义一个同名且修饰符相同的属性,总可以了吧?
class Person{
private age:number;
constructor(age:number) {
this.age = age;
}
}
class Web extends Person{
private age:number
constructor(age:number) {
super(age)
this.age = age;
}
}
let p = new Person(15)
这回就真的是因为private的问题了,我们无法声明一个同名private属性或方法。(退一步讲,如果能声明,那private可就没用了)
(public和protected,因为子类可以用,所以不会出现报错)
get和set方法
看一个例子:
class Person{
constructor(public _age:number) {}
get age(){
return this._age;
}
set age(age:number){
this._age = age;
}
}
const p = new Person(18)
console.log(p.age)
p.age = 21;
可以发现,这么做看起来多此一举,因为最后还是要获取或修改age,我为什么不直接去修改或获取?
其实使用get和set,是可以满足重用性的。比如,每次我都对数据进行处理再返回,我使用get只需要p.age
就能获取,而不是用函数p.age()
。如果我想对传进去的参数进行判断,每个参数都进行判断肯定不行,那我在类外再写个函数判断 不如直接用set判断。
class Person{
constructor(public _age:number) {}
get age(){
// 和普通函数一样,可以对返回值进行设计
// 但它必须返回get到的内容
return this._age + 5;
}
set age(age:number){
// 满足重用性,只要传入值就可以作某些操作
if(age > 18){
this._age = age;
}else{
console.log('年龄不大于18')
}
}
}
const p = new Person(18);
// 我们直接p.age,不用p.age()
console.log(p.age);
// 满足重用性,我们不需要在这里进行age判断
// 在类中就帮我们封装好了方法
p.age = 17;
p.age = 21;
这里补充一点,get可以设置返回值类型,只要能匹配上类型就可以:
get age():number{
return this._age;
}
但是,set是绝对无法设置返回值类型的。
最后要说的是,get和set也可以对private和protected属性生效,即在它们的使用范围外,也可以修改或调用它们的值了:
class Person{
constructor(private _age:number) {}
get age(){
return this._age;
}
set age(age:number){
this._age = age;
}
}
const p = new Person(18);
console.log(p.age);
p.age = 19;
这么做可并没有违背private和protected的初衷噢,虽然它们的本意确实是限制属性的使用范围,但说到底我们还是要使用它们的,使用get和set就可以更好的对它们进行一些封装修饰,然后展示出来或者是存储进去。所以,在类外想要访问它们,就需要使用get和set的类名,得到我们想要的结果了。
静态属性 和 静态方法
先来回顾一下es5中的静态属性和方法:
function Person(){
this.run1 = function(){ // 实例方法
console.log("run1")
}
}
Person.sex = "ls" // 静态属性
Person.run2 = function(){ // 静态方法
console.log("run2")
}
let p = new Person()
p.run1() // 实例方法的调用
console.log(Person.sex) // 静态属性的调用
Person.run2() // 静态方法的调用
在ts中使用静态属性和方法,需要有static关键字,且在静态方法里只能用静态属性:
class Person{
public name:string = "ls"
public sex:string = "男"
static age:number = 20 // 静态属性
constructor(name:string) {
this.name = name
}
run():void{ // 实例方法
console.log(`${this.name}在运动`)
}
static print():void{ // 静态方法
console.log("print方法")
// 静态方法里没办法直接调用类里面的属性
// 报错:
console.log("print:"+this.name) // 输出 print:Person
console.log("print:"+this.sex) // 输出 print:undefined
// 如果想使用类里面的属性,只需要让类里面的属性变为静态属性
console.log("print:"+this.age) // 输出 print:20
}
}
// 静态属性和方法的好处,就是我们不需要去实例化对象,就能直接去使用
console.log(Person.age)
Person.print()
但是从输出结果就可以发现,name这个属性特殊,如果没有静态属性叫name,它会输出类名,同时也是会报错的。那如果我们创建静态属性name,可不可以解决这个问题呢?那我们就可以发现,我们不能取一个静态属性名为name的属性:
静态属性name和其中设置的函数name冲突了,所以这也就是为什么会输出类名。所以之后需要注意,name这个属性名很特殊,它不能设置为静态属性。
只读属性
在类里还有一种修饰属性的符号,它是readonly,只读属性。和它的命名一样,这种属性在我们通过构造函数传递进来后,就无法再修改了:
class Person{
public readonly name:string;
constructor(name:string) {
this.name = name;
}
}
const p = new Person('李四');
p.name = '王五'; // 报错
console.log(p.name);
用set也无法修改噢。
多态
多态:父类定义一个方法但不去实现,让继承它的子类去实现,这样一来每一个子类都可以有不同的表现。所以多态在这里也属于继承。
在最开始学习的时候,可能会觉得奇怪:为什么要创建这么一个看似毫无作用的父类?首先,创建了这样的父类,它本身的代码量很少,方便其它人在看到这个父类的时候,就知道各个功能要实现什么功能,就相当于是个参考文档。并且,让子类去继承这样的父类,也就有了一个参考,避免之后子类误操作创建了同名方法去覆盖父类的方法。我们只需要对其进行扩展即可,而且不同的子类可以为其设置不同的表现效果。
class Animal{
name:string
constructor(name:string) {
this.name = name
}
eat(){
// 之后子类会对其进行改变
console.log("吃的方法")
}
}
class Dog extends Animal{
constructor(name:string) {
// 子类可以写构造函数,也可以不写
// 如果写,那就必须要super
super(name)
}
eat(){
return this.name + "吃肉"
}
}
class Cat extends Animal{
// 子类可以写构造函数,也可以不写
// 如果写,那就必须要super
eat(){
return this.name + "吃鱼"
}
}
let dog = new Dog("旺财")
let cat = new Cat("阿愈")
console.log(dog.eat()) // 输出:旺财吃肉
console.log(cat.eat()) // 输出:阿愈吃鱼
抽象类
ts中的抽象类:它用来提供其他类继承的基类,不能直接被实例化。如果想使用抽象类,用abstract关键字来定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。需要注意,abstract抽象方法只能放在抽象类里面。
abstract class Animal{
public name:string
constructor(name:string) {
this.name = name;
}
abstract eat():any
run(){
console.log("其它方法可以不实现")
}
}
class Dog extends Animal{
constructor(name:string) {
super(name)
}
// 抽象类的子类必须实现抽象方法,否则报错
eat(){
return this.name + "吃肉"
}
}
let cat = new Animal('阿愈'); // 报错:无法直接实例化抽象类
let dog = new Dog("旺财")
console.log(dog.eat()) // 输出:旺财吃肉
那现在回头聊多态,就可以发现,其实多态的用法没那么多要求,因为它更像是自己给自己写的标准,父类其实就是普通的类,子类不需要一定实现父类的函数。而抽象类是ts设计出来让我们来定义标准的,它就会有很多束缚了,一旦出现问题就会报错,相比多态里出现问题可能就无法及时发现问题所在。