类的使用
在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方便的定义和使用类。
实际上在JavaScript的开发过程中,我们更加习惯于函数式编程:
- 比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;
- 比如在Vue3开发中,目前也更加推崇使用 Composition API;
但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们。
类的定义我们通常会使用class关键字:
- 在面向对象的世界里,任何事物都可以使用类的结构来描述;
- 类中包含特有的属性和方法;
类的定义
我们可以声明类的属性:在类的内部声明类的属性以及对应的类型
-
如果类型没有声明,那么它们默认是any的;
-
我们也可以给属性设置初始化值;
-
在默认的
strictPropertyInitialization
模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错;- ✓ 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用
name!: string
语法;
- ✓ 如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用
-
类可以有自己的构造函数constructor,当我们通过new关键字创建
一个实例时,构造函数会被调用; -
构造函数不需要返回任何值,默认返回当前创建出来的实例;
我们发现在构造函数中没有初始化name,那么在定义的时候就需要定义为:!:
类中可以有自己的函数, 定义的函数称之为方法;
类的继承
和JS的语法一样,不过多介绍
成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、 private、 protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
- private 修饰的是仅在同一类中可见、私有的属性或方法;
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
eating()方法变成私有的,外部就不可以访问到了,只有eating所在的类可以访问。
子类是不能访问父类私有的属性的,如果外界想要访问私有属性,需要对应私有类提供set、get方法,和java是一样的。
readonly只读属性
getter、setter
在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。
私有属性我们一般在前面会以_
命名
class Person{
private _name:string;
constructor(name:string){
this._name = name
}
set name(newValue:string){
this._name=newValue
}
get name(){
returh this._name
}
}
const p = new Person("why")
p.name = "kobe" // 赋值
console.log(p.name) // 输出
因为一般情况下,我们不希望可以直接访问类的属性,而使用setter、getter,可以对属性的访问进行拦截操作。
class Person{
private age:string;
constructor(age:string){
this.age = age
}
set age(newValue:string){
// 拦截
if(newValue>=0&&newValue<200){
this._age=newValue
}
}
get age(){
returh this.age
}
}
这样我们在进行年龄设置的时候,就可以防止直接修改类时,不符合逻辑值的操作。
通过setter拦截;
参数属性
TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。(可以看做一个语法糖)
- 这些就被称为参数属性(parameter properties);
- 你可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符;
class Person{
age:number
height:number
// 语法糖:直接在这里写上修饰符,就等同于在 类和构造器中 同时定义了name:string
constructor(public name:string,age:number,height:number){
this.age=age
this.height=height
}
}
// 正常使用
const p = new Person("why",18,1.88)
p.name = "abc"
如果提供的private修饰符,那么外部想要访问,就得通过setter、getter了;
如果是readonly修饰符,则只能访问,不可以写入。
抽象类abstract
我们知道,继承是多态使用的前提。
- 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
- 抽象方法,必须存在于抽象类中;
- 抽象类是使用abstract声明的类;
抽象类有如下的特点:
- 抽象类是不能被实例的话(也就是不能通过new创建)
- 抽象方法必须被子类实现,否则该类必须是一个抽象类;
abstract class Shape {
// getArea方法只有声明没有实现体
// 实现让子类自己实现
// 可以将getArea方法定义为抽象方法: 在方法的前面加abstract
// 抽象方法必须出现在抽象类中, 类前面也需要加abstract
abstract getArea()
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super()
}
getArea() {
return this.width * this.height
}
}
class Circle extends Shape {
constructor(public radius: number) {
super()
}
getArea() {
return this.radius ** 2 * Math.PI
}
}
class Triangle extends Shape {
getArea() {
return 100
}
}
没有方法体的就是抽象方法,前提是必须存在抽象类中,使用abstract
声明抽象
由于上面的类都是继承Shape抽象类的,所以可以作为参数:
// 通用的函数:要求传入Shape类型,也就是shape的子类
function calcArea(shape: Shape) {
return shape.getArea()
}
calcArea(new Rectangle(10, 20))
calcArea(new Circle(5))
calcArea(new Triangle())
// 在Java中会报错: 不允许
calcArea({ getArea: function() {} })
// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc")
抽象类是不可以被实例化的!!!因为抽象类中存储抽象方法,而抽象方法是由子类实现的,所以我们应该实例化的是子类!!!
const shape1:Shape = new Circle(5)
那么这个就是父类的引用,指向了子类的对象,这就是多态。
TS的鸭子类型:
TypeScript对于类型检测的时候使用的鸭子类型
- 鸭子类型: 如果一只鸟, 走起来像鸭子, 游起来像鸭子, 看起来像鸭子, 那么你可以认为它就是一只鸭子
- 鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型
class Person {
constructor(public name: string, public age: number) {}
running() {}
}
class Dog {
constructor(public name: string, public age: number) {}
running() {}
}
function printPerson(p: Person) {
console.log(p.name, p.age)
}
printPerson(new Person("why", 18))
printPerson({name: "kobe", age: 30, running: function() {}})
---------------------------------
printPerson(new Dog("旺财", 3))
const person: Person = new Dog("果汁", 5)
横线分割后的就是所谓的鸭子类型,printPerson明明要求传入的是一个Person类型,结果你传一个Dog类型的居然也算对,那是因为这两个类型中要求的属性是一样的;
但是在java中,如果类型不一样的话,是不允许传的,在TS中是可以的,只要在TS中定义的属性和行为相同。和传统的面向对象语言是有区别的。
类的类型
类本身也是可以作为一种数据类型的
class Person{
}
const name: string = "aaa"
const p: Person = new Person()
// 类可以作为一个类型
function printPerson(p:Person){}
export{}
这里p:Person
Person就是一个类型
类的作用:
- 可以创建类对应的实例对象
- 类本身可以作为这个实例的类型
- 类也可以当做一个有构造签名的函数
// factory()函数要求传入的参数是一个构造函数
function factory(ctor: new()=>void){}
const p:Person = new Person()
factory(Person)
类可以被当成一个有构造签名的函数
对象类型的属性修饰符
对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。
type IPerson = {
// 属性 ?: 表示可选属性
name?:string
// readonly:只读属性
readonly age:number
}
interface IKun{
name:string
slogan:string
}
const p:IPerson = {
name:"why",
age:18
}
索引签名
什么是索引签名呢?
- 有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征;
- 这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型;
一个索引签名的属性类型必须是 string 或者是 number。
- 虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型;(了解)
interface ICollection {
// 索引签名:我能通过数字类型的索引,访问到string类型的值
[index:number]:string
length:number
}
const names:string[] = ["aaa","bbb","ccc"]
// 传入一个集合,并遍历
function iteratorCollection(collection:ICollection){}
// 这样我用索引也可以访问
iteratorCollection(names)
const tuple:[string,number] = ["why",18]
iteratorCollection(tuple)
- 索引签名的理解:
function getInfo(){
const abc:any="aaa"
return abc
}
const info = getInfo()
// 我们不知道这个info里面有啥属性,不过我们可以知道特点是什么
----------------------------------
可以使用索引签名:可以通过字符串索引,去获取到一个字符串值
interface InfoType{
[key:string]:string
}
function getInfo():InfoType{
const abc: any = "aaa"
return abc
}
const info = getInfo()
// 这样就可以通过字符串索引,来访问到值了
console.log(info["name"], 也支持写法: info.age)
- 索引签名的理解
// 索引签名:根据数字类型的下标,取到一个字符串类型的值
interface ICollection{
[index:number]:string
length:number
}
function printCollection(collection:ICollection){
for(let i=0;i<collection.length;i++){
const item=collection[i]
}
}
const array = ["abc","cba","nba"]
const tuple: [string,string] = ["why","广州"]
printCollection(array)
printCollection(tuple)
export {}
可以通过索引下标拿到字符串类型的数值,并且这两个都有length属性。
索引签名是为了编写通用的接口
接口继承
接口和类一样是可以进行继承的,也是使用extends关键字:
- 并且我们会发现,接口是支持多继承的(类不支持多继承)
- 可以从其他接口中,继承过来属性
- 减少了相同代码的重复编写
- 我们可以自定义一个接口,并且可以拥有一些第三方库中的某些属性;
interface IPerson{
name:string
age:number
}
// 接口的继承
interface IKun extends IPerson{
slogan:string
}
const ikun:IKun = {
name:"aaa",
age:18,
slogan:"你干嘛"
}
export {}
接口的实现过程
类必须要实现接口,这样一来,类就具备了该接口所有的特性:const ikun2 = new Person()
- 接口可以作为类型,进行限制
- 接口可以被类实现
interface IKun {
name: string
age: number
slogan: string
playBasketball: () => void
}
interface IRun {
running: () => void
}
const ikun: IKun = {
name: "why",
age: 18,
slogan: "你干嘛!",
playBasketball: function() {}
}
// 作用: 接口被类实现,并且实现多个接口
class Person implements IKun, IRun {
name: string
age: number
slogan: string
playBasketball() {
}
running() {
}
}
const ikun2 = new Person()
const ikun3 = new Person()
const ikun4 = new Person()
console.log(ikun2.name, ikun2.age, ikun2.slogan)
ikun2.playBasketball()
ikun2.running()
抽象类和接口的区别(了解)
枚举类型
enum Direction{
UP,
DOWN,
LEFT,
RIGHT
}
// d1是一个枚举类型的标识符,到底是什么类型
// 需要我们手动指定
const d1:Direction = Direction.UP
方向的移动经常会用到枚举类型:
// 定义枚举类型
enum Direction {
LEFT,
RIGHT
}
const d1: Direction = Direction.LEFT
function turnDirection(direction: Direction) {
switch(direction) {
case Direction.LEFT:
console.log("角色向左移动一个格子")
break
case Direction.RIGHT:
console.log("角色向右移动一个格子")
break
}
}
// 监听键盘的点击
turnDirection(Direction.LEFT)
枚举类型的值
使用位运算符的写法:了解即可,这个是计算机基础的知识,在写框架的时候,有些大佬喜欢这样写枚举:
enum Direction{
Read= 1 << 0,
Write = 1 << 1,
Foo = 1 << 2
}
操作系统有提供计算器供我们参考: