类
有关js中类的知识就不介绍了。只介绍一些typescript中对类的扩展。
类的成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的。
- private 修饰的是仅在同一类中可见、私有的属性或方法。
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
只读属性
如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly。但是只读属性可以在constructor中对其赋值。属性本身不能进行修改, 但是如果它是对象类型, 对象中的属性是可以修改。
class Person { // 1.只读属性是可以在构造器中赋值, 赋值之后就不可以修改 // 2.属性本身不能进行修改, 但是如果它是对象类型, 对象中的属性是可以修改 readonly name: string age?: number readonly friend?: Person constructor(name: string, friend?: Person) { this.name = name this.friend = friend } } const p = new Person("llm", new Person("jcl")) console.log(p.name) console.log(p.friend) // 不可以直接修改friend // p.friend = new Person("hcy") if (p.friend) { // name也是不能修改的,因为name是只读的。 p.friend.name = "zh" // 但是age是可以修改的 p.friend.age = 30 } // 不能修改 // p.name = "123"
存取器(getter, setter)
在前面一些私有(private
)属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。
class Person { // 定义私有属性 private _name: string constructor(name: string) { this._name = name } // 访问器setter/getter // setter set name(newName) { this._name = newName } // getter get name() { return this._name } } const p = new Person('zh') p.name = 'llm' console.log(p.name)
抽象类
我们知道,继承是多态使用的前提。所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
什么是 抽象方法?
在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。抽象方法,必须存在于抽象类中。抽象类是使用abstract声明的类。
抽象类有如下的特点:
- 抽象类是不能被实例化的(也就是不能通过new创建)。
- 抽象方法必须被子类实现,否则该类必须是一个抽象类。
// 这里不能传入Shape类实例化的对象。因为Shape是抽象类 function makeArea(shape: Shape) { return shape.getArea() } abstract class Shape { abstract getArea(): number } class Rectangle extends Shape { private width: number private height: number constructor(width: number, height: number) { super() this.width = width this.height = height } getArea() { return this.width * this.height } } class Circle extends Shape { private r: number constructor(r: number) { super() this.r = r } getArea() { return this.r * this.r * 3.14 } } const rectangle = new Rectangle(20, 30) const circle = new Circle(10) console.log(makeArea(rectangle)) console.log(makeArea(circle))
类作为类型
类也可以作为一个类型使用
class Person { name: string = "zh" eating() { } } const p = new Person() // 用类来约束p1 const p1: Person = { name: "llm", eating() { } } function printPerson(p: Person) { console.log(p.name) } printPerson(new Person()) printPerson({name: "llm", eating: function() {}})
接口
接口定义对象类型(只读, 可选)
可以声明对象类型, 并且可以指定可读属性readonly
和可选属性?
interface IInfoType { readonly name: string age: number, fn: () => void } const info: IInfoType = { name: "zh", age: 20, fn() { console.log(name, age) } }
定义索引类型
我们可以定义相同类型的键值和键名。
// 通过interface来定义索引类型 interface IndexLanguage { [index: number]: string } const frontLanguage: IndexLanguage = { 0: "HTML", 1: "CSS", 2: "JavaScript", 3: "Vue" } interface ILanguageYear { [name: string]: number } const languageYear: ILanguageYear = { "C": 1972, "Java": 1995, "JavaScript": 1996, "TypeScript": 2014 }
接口定义函数类型
我们可以通过interface来定义对象中普通的属性和方法的,实际上它也可以用来定义函数类型。没有方法体,只有方法签名。但是还是通过类型别名来定义函数类型较好。
// type CalcFn = (n1: number, n2: number) => number // 可调用的接口 interface CalcFn { (n1: number, n2: number): number } function calc(num1: number, num2: number, calcFn: CalcFn) { return calcFn(num1, num2) } const add: CalcFn = (num1, num2) => { return num1 + num2 } calc(20, 30, add)
接口的继承
接口和类一样是可以进行继承的,也是使用extends关键字。并且可以支持多继承。
interface ISwim { swimming: () => void } interface IFly { flying: () => void } interface IAction extends ISwim, IFly {} const action: IAction = { swimming() {}, flying() {}, }
类可以实现接口
接口定义后,也是可以被类实现的。如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入。这就是面向接口开发。
我们可以通过implements
来实现接口。
interface ISwim { swimming: () => void } interface IEat { eating: () => void } // 类实现接口 class Animal {} // 继承: 只能实现单继承 // 实现: 实现接口, 类可以实现多个接口 class Fish extends Animal implements ISwim, IEat { swimming() { console.log('Fish Swmming') } eating() { console.log('Fish Eating') } } class Person implements ISwim { swimming() { console.log('Person Swimming') } } // 编写一些公共的API: 面向接口编程 function swimAction(swimable: ISwim) { swimable.swimming() } // 1.所有实现了接口的类对应的对象, 都是可以传入 swimAction(new Fish()) swimAction(new Person()) swimAction({ swimming: function () {} })
interface和type区别
- 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function。
- 如果是定义对象类型,那么他们是有区别的:
- interface 可以重复的对某个接口来定义属性和方法。ts会合并同名的接口,我们在实现的时候,需要实现合并到接口。
interface IFoo { name: string } interface IFoo { age: number } // 这里需要实现上面的 const foo: IFoo = { name: 'zh', age: 18, }
- type定义的是别名,别名是不能重复的。
// 重复定义会报错 type IBar = { name: string age: number } type IBar = { }
引用赋值
当我们引用赋值对象给不同接口的时候,ts内部会处理不同接口类型的差异,不会报错。如果直接将不同接口的对象赋值其他接口,将会报错。
interface IPerson { name: string age: number } const info = { name: 'zh', age: 20, address: '信阳市', } // freshness擦除, 不会报错 const p: IPerson = info // 这样会报错,因为赋值的对象类型和IPerson类型不符 const p1: IPerson = { name: 'zh', age: 20, address: '信阳市' }
枚举类型
通过enum
来定义枚举类型。其实他也可以通过联合类型来代替。
- 枚举类型的值默认是从0开始的递增。
- 枚举类型(只针对数字枚举)还可以反向取值,利用下标,取出对应的枚举值。
enum Direction { LEFT, RIGHT, TOP, BOTTOM, } console.log(Direction[2]) // TOP
- 枚举类型的属性值,可以是数字,也可以是字符串。
- 不会为字符串枚举成员生成反向映射。
泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性: 比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作。
泛型在函数中的使用
以前我们都是在函数定义的时候,给函数参数和返回值做类型约束,但是现在我们可以通过泛型来对其做类型约束。我们在定义的时候给定参数泛型变量,等我们调用函数的时候传入泛型变量类型即可。使参数的类型是也可以参数化。 一般在我们传入函数参数的时候,ts可以自动推断出泛型变量的类型,所以可以不显示的传入泛型类型。
function sum<Type>(num: Type): Type { return num } // 1.调用方式一: 明确的传入类型 sum<number>(20) sum<{name: string}>({name: "zh"}) sum<any[]>(["abc"]) // 2.调用方式二: 类型推到 sum(50) sum("abc")
并且,我们可以定义多个泛型。注意:在传入类型的时候要么都传入要么都不传入。
function foo<T, E, O>(arg1: T, arg2: E, arg3?: O, ...args: T[]) {} // 这里要么多传入类型,要么都不传 foo<number, string, boolean>(10, 'abc', true)
如果在函数中给泛型指定默认类型,我们必须全部指定,要不然会报错。(这是错误的,请忽略)
// 这里的泛型类型只要指定了其中一个,其他得也得指定。 function foo<T = string, E = string, O = boolean>( arg1: T, arg2: E, arg3?: O, ...args: T[] ) {} foo<number, string, boolean>(10, 'abc', true)
平时在开发中我们可能会看到一些常用的泛型名称:
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对
- E:Element的缩写,元素
- O:Object的缩写,对象
泛型在接口中的使用
我们可以在接口中使用泛型,来让外界自己传入指定的类型来约束内部的属性或方法。
并且他不会自动推断泛型的类型。
interface IPerson<T1, T2> { name: T1 age: T2 } const p: IPerson<string, number> = { name: "zh", age: 20 }
我们也可以指定泛型的默认类型,并且可以部分指定。
interface IPerson<T1, T2 = number> { name: T1 age: T2 } // 只传入第一个类型,第二个用指定的类型 const p: IPerson<string> = { name: 'zh', age: 20, }
泛型在类中的使用
我们可以在调用类后面传入类型,也可以在指定实例化对象类型的时候传入类型。并且,如果不传入类型,ts也可以进行类型推断。并且也可以指定默认的类型。
class Point<T = number> { x: T y: T z: T constructor(x: T, y: T, z: T) { this.x = x this.y = y this.z = y } } const p1 = new Point('1.33.2', '2.22.3', '4.22.1') const p2 = new Point<string>('1.33.2', '2.22.3', '4.22.1') const p3: Point<string> = new Point('1.33.2', '2.22.3', '4.22.1')
泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中。这时候我们可以让泛型继承extends
自这个类型。
比如string和array都是有length的,或者某些对象也是会有length属性的。那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?
interface ILength { length: number } function getLength<T extends ILength>(arg: T) { return arg.length } getLength("abc") getLength(["abc", "cba"]) getLength({length: 100})
模块解析
类型查找
之前我们所有的typescript中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型:
const image = document.getElementById('image') as HTMLImageElement
大家是否会奇怪,我们的HTMLImageElement类型来自哪里呢?甚至是document为什么可以有getElementById的方法呢?
其实这里就涉及到typescript对类型的管理和查找规则了。我们这里先给大家介绍另外的一种typescript文件:.d.ts
文件
我们之前编写的typescript文件都是 .ts
文件,这些文件最终会输出 .js
文件,也是我们通常编写代码的地方。还有另外一种文件 .d.ts
文件,它是用来做类型的声明(declare)。 它仅仅用来做类型检测,告知typescript我们有哪些类型。
那么typescript会在哪里查找我们的类型声明呢?
- 内置类型声明。
- 内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件。
- 包括比如Math、Date等内置类型,也包括DOM API,比如Window、Document等。
- 外部定义类型声明。通常是我们使用一些库(比如第三方库)时,需要的一些类型声明。
- 有时候安装第三方包,他自己内部就定义了
.d.ts
文件。例如axios库。
- 大部分情况下,第三方库都没有提供内置
.d.ts
文件,我们可以通过该链接查找对应库的.d.ts
来进行安装。
- 自己定义类型声明。 下面我们就来介绍自定义类型声明。
声明模块
什么情况下需要自己来定义声明文件呢?
- 我们使用的第三方库是一个纯的JavaScript库,没有对应的声明文件。 可以通过声明模块的语法:
declare module '模块名' {}
- 在声明模块的内部,我们可以通过 export 导出对应库的类、函数等。
declare module 'lodash' { export function join(arr: any[]): void }
- 我们给自己的代码中声明一些类型,方便在其他地方直接进行使用。就是将声明变量/函数/类等的类型声明和实现分开写。
// 声明变量/函数/类 declare let name: string declare let age: number // 声明函数 declare function foo(): void // 声明类 declare class Person { name: string age: number constructor(name: string, age: number) }
- 声明文件,typescript也不识别导入的图片文件等,所以我们也需要声明。
// 声明文件 。当我们在ts文件中引入图片等文件时,会报错。 declare module '*.jpg' declare module '*.jpeg' declare module '*.png' declare module '*.svg' declare module '*.gif'
- 声明命名空间
// 声明命名空间 declare namespace $ { export function ajax(settings: any): any }
上面类型的具体实现
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js "></script>
let name = 'zh' let age = 20 function foo() { console.log('foo') } class Person { name age constructor(name, age) { this.name = name this.age = age } } console.log(name) console.log(age) foo() const p = new Person('llm', 20) console.log(p) $.ajax({})