TypeScript

认识TypeScript

了解TS

TypeScript是Microsoft公司注册商标。

TypeScript具有类型系统,且是JavaScript的超集。 它可以编译成普通的JavaScript代码。 TypeScript支持任意浏览器,任意环境,任意系统并且是开源的。

TypeScript目前还在积极的开发完善之中,不断地会有新的特性加入进来。 因此本手册也会紧随官方的每个commit,不断地更新新的章节以及修改措词不妥之处。

安装TS

有两种主要的方式来获取TypeScript工具:

  • 通过npm(Node.js包管理器)
  • 安装Visual Studio的TypeScript插件

针对使用npm的用户:

1
npm install -g typescript

简单运行TS文件

新建一个index.ts文件

1
2
3
4
5
6
7
function greeter(person) {
return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

在命令行中,使用tsc index.ts 来编译ts文件 —->生成同名的js文件

image-20240908151425057

image-20240908151435872

自动化编译TypeScript

因为ts不能够直接运行,需要我们手动转为js才能运行,为了方便开发者,我们需要将自动转为js这一操作开启。

  1. 命令行输入tsc --init我们会得到一个名为tsconfig.json文件。该文件配置了ts转为js时的一些规则,比如转换成那个规范(es5,es6,es7)等。

  2. 使用命令tsc --watch,表示监视某个ts文件,当文件内容发生变化时,就转为js,如果不指定文件那该命令就是监视所有的ts文件,一旦发生变化就自动进行转换。

还有一点,当我开启全局监视时,确实完成了自动ts转js的功能,但是当我们ts文件中写的内容发生错误时,它依旧会直接进行转换,这个是我们不希望的。解决如下:

  1. 打开tsconfig.json文件查找noEmitOnError,将其注释去掉即可。该配置项意味着出现错误就不要再提交进行转换了。

类型声明

基础语法:let 变量:变量类型

1
2
3
let a: string;
let b: number;
let c: boolean;

除了能定义变量,还能定义函数参数的类型和函数的返回值的类型,比如

1
2
3
function add(x: number, y: number): number {
return x + y;
}

这表示你要接受两个类型是数字型的参数。,并且返回值的类型也是数字型。

类型推断

TypeScript类型推断简介

类型推断是指TypeScript编译器自动地根据赋值来确定一个值的类型。这意味着在某些情况下,你不需要显式地指定类型,TypeScript可以根据上下文来推断出正确的类型。

1. 变量声明

当你声明一个变量并立即赋予一个值时,TypeScript会根据这个值的类型来推断该变量的类型。

1
let myVariable = "Hello World"; // myVariable被推断为string类型

2. 函数参数

在函数参数中,如果没有显式地指定类型,TypeScript也会尝试根据函数体内的操作进行类型推断。

1
2
3
function logMessage(message) {
console.log(message.length); // message被推断为string类型
}

3. 函数返回值

当函数的返回值没有明确指定类型时,TypeScript会根据return语句的结果来推断返回值类型。

1
2
3
function getFirstElement(arr) {
return arr[0]; // 如果arr是一个数组,TypeScript将根据arr[0]的类型来推断getFirstElement的返回类型
}

4. 数组

对于数组,类型推断会根据初始化时的元素来确定数组元素的类型。

1
let numbers = [1, 2, 3]; // numbers被推断为number[]

5. 对象字面量

当定义一个对象字面量时,TypeScript会根据提供的键值对来推断对象的类型。

1
let person = { name: "Alice", age: 25 }; // person被推断为{ name: string; age: number }

注意事项

虽然类型推断可以使代码更加简洁,但在某些情况下,显式地指定类型可能会更加清晰并且有助于避免潜在的错误。因此,在使用类型推断时,应权衡其带来的便利性和可能的混淆性。

显式类型声明 vs 类型推断

  • 显式类型声明:在声明时就清楚地定义了类型,适用于复杂的逻辑和需要明确类型的情况。
  • 类型推断:减少了代码量,适用于简单和直白的情况。

类型总览

JavaScript数据类型

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. bigint
  7. symbol
  8. object

其中object包含:Array,Function,Date,Error等……

TypeScript数据类型

  1. 上述js中的所有类型

  2. 六个新类型

    any

    unknow

    never

    void

    tuple

    enum

  3. 新增了两个用于自定义类型的方式

    type

    interface

注意点

官方推荐在指定类型时,使用小写,比如

1
2
let a: string;
let b: number;

image-20240908155452845

原始类型 VS 包装对象

image-20240908155614384

自动装箱

JavaScript在必要时会将自动原始类型包装成对象,以便调用方法或访问属性。

类型断言

什么是类型断言?

类型断言(Type Assertion)是一种告诉编译器“你应该将此值视为这种类型”的方式。通常,当你从一个上下文获取一个值时,这个值可能是多种类型之一,这时你就需要使用类型断言来明确地指定这个值应该被视为哪种类型。

使用场景

类型断言常用于以下几种情况:

  • 当你知道一个值的真实类型,但是TypeScript无法推断出来时。
  • 在使用JavaScript的库时,这些库可能没有类型信息,或者类型信息不准确。
  • 需要在联合类型或任何类型(any)之间进行转换。

断言语法

TypeScript提供了两种形式的类型断言:

  1. 尖括号语法

    1
    2
    let someValue: any = "this is a string";
    let strLength: number = <string>someValue.length;
  2. as 关键字语法

    1
    2
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;

示例

假设你有一个函数,它可能返回多种类型的值,你想要使用其中一个类型的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getFirstElement(array: any[]): any {
return array[0];
}

let myArray = [1, "hello", true];

let element = getFirstElement(myArray);

// 这里element的类型是any,因为我们不知道myArray的第一个元素是什么类型
// 如果我们知道第一个元素总是number类型,我们可以使用类型断言
let firstElement: number = element as number;

console.log(firstElement.toFixed()); // 这样就可以调用number的方法

注意事项

  • 类型兼容性:断言只能在兼容的类型之间进行。例如,你不能将一个string断言为boolean
  • 滥用any类型:尽量避免使用any类型,因为这会绕过TypeScript的所有类型检查。如果必须使用any,在可能的情况下尽快将其断言回一个具体的类型。
  • 类型安全:类型断言不会改变运行时的实际值,只影响TypeScript编译时的类型检查。如果断言错误,可能会导致运行时错误。

常用类型

any

any:含义是任意类型,一旦将变量类型限制为any,那就意味着放弃了对该变量的类型检查

  • any类型的变量可以赋值给任意类型的变量。!!!

unknown

unknown:含义是未知类型。

  • unknow可以理解为一个类型安全的any,适用于:不确定数据的具体类型。

  • 当我们给某个变量类型定义为known时,会出现一些问题(比如:”str的类型为未知”),这时候解决问题有三种方法:1.使用条件判断。2.使用类型断言,明确数据类型。

never

never:含义是任何值都不是,简单说就是不能有值,undefinednull' '0都不行!

  • 实际开发中,几乎不使用never去限制变量类型
  • never一般是TS主动推断出来的。
  • never可以用来限制函数的返回值

void

void:通常用于函数返回值的声明。含义:函数返回值为空,调用者也不应该依赖于函数的返回值去做任何操作。

  • undefined是void可接受的一种空。
  • 若函数的返回值是void,则:1.从语法上讲:函数可以返回undefined,至于显式返回还是隐式返回,无所谓。2.函数调用者不应该关心函数的返回值,也不应该依赖于函数的返回值去做任何操作。即使明白该函数返回了undefined

object和Object

object:(该类型开发不常用,因为限制的范围太大了)该类型的含义是:能够存储非原始类型数据。

Object:该类型的含义是:除了null类型和undefined类型,其他类型的数据都可以存储。

拓展1:声明对象类型

1
2
3
4
5
6
7
8
9
10
11
//声明对象类型的数据 - 用逗号 ?表示age是可选属性
let person1: { name: string, age?: number }
//还可以这样写 - 使用 ;
let person2: { name: string; age?: number; }
//也可以这样写 - 使用换行
let person3: {
name: string
age?: number
}
//如果想要追加属性可以使用索引签名 - [key:string]:any
let person1: { name: string, age?: number,[key:string]:any }

拓展2:声明函数类型

1
2
//let 函数名 = (函数参数) => 返回值类型
let count = (a:number,b:number) => number

通过上面那样定义,表示:该变量count,接受一个返回值是number类型,且携带了a,b两个数字类型的参数的函数。

拓展3:声明数组类型

1
2
let arr = string[] // - 表示声明了一个存储字符串类型的数组。
let arr1 = Array<number> // - 表示声明了一个存储数字型数据的数组。

基础语法:数据类型[] / Array<数据类型>

tuple

元组(tuple)是一种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型都是已知的且可以不同。元组用于精确描述一组值的类型,?表示可选元素。

基本语法:

1
let arr = [string,number];

这表示该元祖存储的数据类型必须是string和number且string在前,number在后。

enum

枚举类型。枚举可以定义一组命名常量,它能增强代码的可读性,也让代码更好维护。枚举是一个对象。

常量:无法更改其值。

基本语法:enum + 枚举名(一般开头大写) { }

数字枚举

数字枚举存在反向映射。也就是0 - up,1-down,2-left,3-right

1
2
3
4
5
6
enum Direction {
Up,
Down,
Left,
Right
}

查看控制台:

image-20240909202829550

字符串枚举

字符串枚举不存在反向映射

1
2
3
4
enum StrEnum {
name='xiaoli',
age='22'
}

查看控制台:发现并不存在有值 - 键的关系。

image-20240909203441996

常量枚举

常量枚举是一种特殊的枚举类型,它使用const关键字定义,在编译时会被内联,避免生成一些额外代码。

如果我像下面这样写

1
2
3
4
5
6
7
//数字枚举
enum Direction {
up,
down,
left,
right
}

那么生成的js文件是下面这样的:多且杂

1
2
3
4
5
6
7
8
9
"use strict";
//数字枚举
var Direction;
(function (Direction) {
Direction[Direction["up"] = 0] = "up";
Direction[Direction["down"] = 1] = "down";
Direction[Direction["left"] = 2] = "left";
Direction[Direction["right"] = 3] = "right";
})(Direction || (Direction = {}));

如果我像下面这样写 - 这种写法就被称为常量枚举。

1
2
3
4
5
6
7
//数字枚举
const enum Direction {
up,
down,
left,
right
}

生成的js文件就是这样的

1
console.log(0 /* Direction.up */); //其实是个对象

type

在TypeScript中,type关键字用于创建类型别名(Type Aliases),这可以帮助简化复杂的类型名称,使得代码更加清晰易读。下面是一些关于TypeScript中的type类型的简要学习笔记:

1. 基础类型定义

TypeScript 中的基础类型包括 string, number, boolean, null, undefined, void, any, unknown 等。你可以为这些基础类型创建一个别名:

1
2
type Name = string;
let userName: Name = "Tom";

2. 联合类型

联合类型允许将几种类型合并为一种类型

1
2
3
type ID = number | string;
let myId: ID = 42;
myId = "str42"; // 合法的,因为ID可以是string或number

3. 交叉类型

交叉类型允许组合多种类型来形成一个新的类型:

1
2
3
4
5
6
7
8
9
10
11
type Person = {
name: string;
};
type Employee = {
employeeId: number;
};
type EmployeePerson = Person & Employee;
let employee: EmployeePerson = {
name: 'John Doe',
employeeId: 12345
};

4. 类型别名与接口

类型别名和接口都可以用来描述对象的形状,但是它们有一些不同之处。类型别名可以合并类型,而接口则可以扩展:

1
2
3
4
5
6
7
8
type Animal = { name: string; };
interface Animal {
age?: number;
}
// 或者使用extends
interface Animal extends { name: string } {
age?: number;
}

一个特殊况情况

case1:声明函数的同时限制返回值类型为void

看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
//定义函数的同时也限制返回值类型 - void 并声明函数。
function Hello():void{

//如果我们这样子写了,那么该函数只能返回undefined ,不能返回其他类型的值
/*
return 11;
return false;
return null;
return "hello"
上述写法全部报错类型分配错误
*/
}

case2:先定义函数的类型和声明返回值为void,然后再利用变量去声明函数。

1
2
3
4
5
6
7
8
9
10
11
12
//如果我们先定义函数和声明返回值类型,在进行函数的声明,就比如
type testFn = () => void; //定义一个类型是testFn 且返回值类型是void的函数

//利用变量去声明该函数
const f1:testFn = function() {
/*
如果向这样子写了,那么该函数返回任何类型的值都可以,就好像失去了void返回值类型的限制
return 11;
return true;
均不会报错。
*/
}

上述看着好像没多大问题,但是实际上,问题就在于如果我们拿到了这个返回值,并使用返回值进行一些逻辑编写,还是会报错。比如

1
2
3
if(f1()){
//这就会发生报错 - 无法测试"void"类型的表达式的真实性
}

这点要注意了。

类相关知识

最基本类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
name:string,
age:number
//使用构造器来
constructor(name:string,age:number){
this.name = name;
this.age = age;
}
//类方法
speak(){
console.log(`我叫${this.name},今年${this.age}岁`);
}
}

//使用类
const p1 = new Person('张三',18);

类的继承

使用extends来继承父类。

注意点:

  • 如果想要在子类中添加新的属性,需要利用constructor构造器来添加,并且构造器中必须包含父类中已有的属性,随后在添加子类独有的属性。
  • 在子类中能够重写父类的方法。注意要同名,如不同名则会被TS认为是子类独有方法。并利用override修饰重写的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//该类继承上面的Person类
class Student extends Peoson{
//类独有属性
grade:string,
//通过构造器注入 - 不要丢掉父类中的属性
constructor(name:string,age:number,grade:string){
//先调用父类
super(name,age);
//在子类
this.grade = grade;
}
//重写父类方法直接同名即可覆盖 注意加上override来修饰方法
override speack(){
console.log(`我是子类中重写的方法,我叫${this.name},今年${this.age}岁,来自于班级${this.grade}`)
}
}

上述代码中的super方法实际上是在调用父类中的constructor方法

属性修饰符

关键字 描述 示例
public 默认的可见性级别,表示该成员可以在任何地方被访问。
class MyClass {
public myProperty = 'Hello World';
}
private 表示只有在声明它的类内部可以访问该成员。
class MyClass {
private myProperty = 'Secret';
}
protected 表示该成员可以在声明它的类和其子类中访问。
class BaseClass {
protected myProperty = 'Shared';
}
class DerivedClass extends BaseClass {}
readonly 表示该成员一旦初始化后就不能被重新赋值。
class MyClass {
readonly myProperty: string;
constructor() {
this.myProperty = 'Immutable';
}
}
static 表示该成员属于类本身,而不是类的实例。
class MyClass {
static myStaticMethod() {
console.log('Called static method');
}
}

引入修饰符后我们可以引出新概念:属性的简写形式

属性的简写形式

简写前:

1
2
3
4
5
6
7
8
9
class Person {
name:string,
age:number
//使用构造器来注入
constructor(name:string,age:number){
this.name = name;
this.age = age;
}
}

简写后:

1
2
3
class Person {
constructor(public name:string,public age:string){}
}

可以发现,通过修饰符修饰属性,就省略了属性的声明和赋值操作,直接大大减少代码量。以后要多用。

抽象类

抽象类是通过关键字abstract来声明的,抽象类是一类无法被实例化的类,专门用来定义类的结构和行为,类中可以写抽象方法,也可以写具体实现。抽象类主要用来为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法。

抽象类概念

简单理解:抽象类不能被实例化,可以继承,抽象类里面可以有普通方法,也可以有抽象方法

抽象方法要用abstract来修饰,并且抽象函数不需要函数体,比如,抽象方法更像是一种形式,我并没有具体要怎么做,我只是声明了一个方法。

1
2
3
4
5
6
7
8
9
10
abstract class Package {
//构造方法注入属性。
constract(public weight:number){}
//抽象方法
abstract calculate():number
//抽象类中也可以存在具体方法。
printPackage(){
console.log(`包裹重${this.weight}KG,花费${this.calculate()}元`)
}
}

抽象类的使用

想要使用抽象类,就必须通过继承来实现。继承的关键字是extends,来看下面这个例子。

1
2
3
4
5
6
7
8
9
10
11
class StandardPackahge extends Package {
//继承就要注意父类中的属性不能丢掉,同时子类也可以有自己的属性
constructor(weight:number,public initPrice:number){
//特别注意这里要使用super方法来给父类传入weight
super(weight);
}
//定义方法 - 实现抽象类中的方法
calculate():number {
return this.weight * this.initPrice
}
}

何时使用抽象类?

  1. 定义通用接口:为一组相关的类定义通用的行为(方法或属性)时。
  2. 提供基础实现:在抽象类中提供某些方法或为其提供基础实现,这样派生类就可以继承这些实现了。
  3. 确保关键实现:强制派生类实现一些关键行为。
  4. 共享代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复。

interface接口

interface是一种定义结构的方式,主要作用是为:类,对象,函数等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意interface只能定义格式,不能包含任何实现!

就拿在AI图生图项目中,定义的一组表单接口如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface formType {
init_images: string[];
sd_model_checkpoint: string;
refBd: boolean;
refHair: boolean;
//使用索引签名 来限制 所有键必须是字符串类型,这样可以动态的向该对象添加属性名和属性值。
prompt: { [index: string]: number };
steps: number;
cfg_scale: number;
sampler_index: string;
seed: number;
negative_prompt: { [key: string]: number };
pictureNum: number;
denoising_strength: number;
width: number;
height: number;
mask: null,
mask_blur: null,
inpainting_mask_invert: null
}

接口定义类

利用implements关键字来实现这个类

1
2
3
4
5
6
interface PersonInterface {
name:string,
age:number,
//定义了一个speack函数,返回值类型是void
speack(n:number) => void,
}

上面定义了类接口,下面就实现这个类接口 - 通过implements关键字

1
2
3
4
5
6
7
8
9
class Person implements PersonInterface {
name:string,
age:number,
speack(n:number){
for(i = 0; i<n; i++){
console.log('1');
}
}
}

接口定义对象

在利用接口定义一个对象类型是,接口更像是一种类型。使用方式也和类型一样

1
2
3
4
5
6
interface PeosonInterface{
name:string,
age?:number, //可选属性
gender:number,
run(n:number) => void
}

上面定义了一个人的接口,使用如下

1
2
3
4
5
6
7
8
const user:PeosonInterface = {
name:'张三'
gender:'男',
age:20,
run(n:number) {
console.log(`跑了${n}米`)
}
}

接口定义函数

当你需要定义一个函数的类型时,可以使用接口来描述这个函数应该接受什么样的参数以及返回什么样的值

1
2
3
4
5
6
7
interface SearchFunc {
(source: string, subString: string): boolean;
}
//下面就来实现该接口
let mySearch: SearchFunc = function (src: string, sub: string):boolean {
return src.search(sub) !== -1;
};

接口可以继承接口

比如说

1
2
3
4
interface Student {
name:string,
age:18,
}

我还有一个接口

1
2
3
4
//让班级接口继承学生接口
interface Grade extends Student {
grade:string
}

image-20240914110837142

可以看到,也就是s1这个对象中,必须有Student接口的属性和Grade接口的属性,缺一不可。

image-20240914110937745

接口自动合并

接口可以进行重复定义:

1
2
3
4
5
6
7
8
interface PersonInterface {
name:string,
age:number
}
//还可以再次定义一个同名接口
interface PersonInterface {
gender:string
}

最终使用该接口时,就要求包含这三个属性,也就是说底层认为,你定义了一个接口

1
2
3
4
5
interface PersonInterface {
name:string,
age:number,
gender:string
}

何时使用接口?

  1. 定义对象的格式:描述数据、API响应格式、配置对象等等…..开发常用
  2. 类的契约:规定一个类中需要哪些属性和方法,以及他们的类型
  3. 自动合并:拓展第三方库的类型。

区分一些相似概念

interface和type的区别

  • 相同点:interface和type都可以用于定义对象结构,在许多场景中可以互换
  • 不同:
    • interface:更专注于对象和类的结构,能够继承和自动合并
    • type:可以定义类型别名、联合类型、交叉类型,不支持继承和自动合并

interface和抽象类的区别

  • 都用于定义一个类的格式(应该遵守的规则)
  • 不同点:
    • 接口:只能描述类的接口,不能有任何实现代码,一个类可以实现(implements)多个接口
    • 抽象类:既可以包含抽象方法,也可以包含具体方法,一个类只能继承(extends)一个抽象类

泛型

泛型允许我们在定义函数、类、接口时,使用类型参数来表示未指定的类型,这些参数在具体使用时,才被指定为具体的类型,泛型能让同一段代码适用多种类型,同时任然保持类型的安全性。我们用<>来表示泛型。

泛型函数

举例:比如在一个函数中:

1
2
3
4
5
6
7
8
//泛型举例
function printData<T>(params:T){
console.log(params);
}

//调用时在具体指定具体类型
printData<number>(10);
printData<string>('father')

泛型函数可以有多个

1
2
3
4
5
6
function logData<T,U>(params1:T,params2:U){
console.log(params1);
console.log(params2);
}
//多个
logData<string,number>('hello',10);

泛型接口

泛型接口也很好理解:关键在于使用时在具体指定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//泛型接口
interface IPerson<T> {
name: string,
age: number,
extraInfo: T
}

//在具体使用时传入具体类型
const p1: IPerson<number> = {
name: '小李',
age: 20,
extraInfo: 10,
}

type jobInfo = {
title:string,
company:string,
}

//在具体使用时传入具体类型
const p2:IPerson<jobInfo> = {
name:'小三',
age:21,
extraInfo:{
title:'高级工程师',
company:'小米'
}
}

泛型类

泛型类:任旧是在具体使用时传入具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type jobInfo = {
title:string,
company:string,
}
//泛型类
class Person<T> {
constructor(
public name:string,
public age:number,
public extraInfo:T
){}
//类方法
speak(){
console.log(`我叫${this.name},今年${this.age}岁`);
console.log(this.extraInfo);
}
}

//具体实现
const p3 = new Person<jobInfo>('小五',30,{title:'高级软件开发工程师',company:'小米公司'})

类型声明文件

在TypeScript中,类型声明文件是用来定义类型信息的特殊文件。这些文件通常用于描述非TypeScript代码(如JavaScript库)的类型结构,或者为项目中的其他部分提供类型信息而不包含任何实现逻辑。类型声明文件通常以.d.ts扩展名结尾,表示“declaration”文件。

类型声明文件的作用

  • 提高开发体验:通过提供类型信息,可以增强IDE的功能,如智能提示、自动补全等。
  • 确保类型安全:当项目使用了类型声明文件后,TypeScript编译器可以在编译阶段检查类型兼容性,提前发现潜在错误。
  • 简化API使用:良好的类型定义可以帮助开发者更快地理解第三方库或模块的API,从而更方便地使用它们。

基本语法

类型声明文件可以包含以下元素:

  • 接口(Interfaces):定义对象的形状。
  • 类型别名(Type Aliases):定义复杂类型的别名。
  • 类类型(Class Types):描述类的静态成员和实例成员。
  • 函数类型(Function Types):定义函数参数和返回值的类型。
  • 变量声明(Variable Declarations):声明全局变量或模块作用域内的变量。
  • 模块声明(Module Declarations):描述全局模块或命名空间。
  • 命名空间(Namespaces):组织相关类型。
  • 枚举(Enums):定义一组命名常量。
  • 导入/导出(Imports/Exports):从其他模块导入类型或向其他模块导出类型。

TypeScript装饰器

  • 装饰器的本质是一种特殊的函数,它可以对:类、属性、方法、参数进行拓展,同时让代码更简洁。
  • 装饰器自2015年在ES6中被提出到现在,已经10年。
  • 截止目前,装饰器依然是实验性特性,需开发者手动调整配置,来开启装饰器。
  • 装饰器有5种:
    • 类装饰器
    • 属性装饰器
    • 方法装饰器
    • 访问器装饰器
    • 参数装饰器

类装饰器

类装饰器是一个应用在类声明上的函数,可以为类添加额外功能,或添加额外逻辑。

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//类装饰器 - 本质是一个函数
//Demo函数会在Person类定义时执行,参数target就是被装饰的类 - Person
function Demo(target: Function) {
console.log(target);
}

@Demo
class Student {
name:string
age:number
constructor(name:string,age:number) {
this.name = name;
this.age = age
}
}

类装饰器应用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//类装饰器 - 本质是一个函数
//Demo函数会在Person类定义时执行,参数target就是被装饰的类 - Person
function Demo(target: Function) {
target.prototype.toString = function() {
//注意这里传递的是实例对象
return JSON.stringify(this)
}
}

@Demo
class Student {
name:string
age:number
constructor(name:string,age:number) {
this.name = name;
this.age = age
}
}
const p4 = new Student('张三',20);


console.log(p4.toString()); //{"name":"张三","age":20}

关于装饰器的返回值

  • 如果装饰器有返回值,且返回值是一个类,那么返回值的类会代替被装饰器装饰的类。
  • 如果装饰器没有返回值或返回undefined,那装饰器的类不会被替换。

未启用装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function test(){
return class {
test2(){
console.log(200);
console.log(300);
console.log(400);

}

}
}
//@test
class Demo {
test() {
console.log(100);

}
}

console.log(Demo);

控制台:

image-20240914170810053

启动装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

function test(target:Function){
return class {
test2(){
console.log(200);
console.log(300);
console.log(400);

}

}
}
// @ts-ignore
@test
class Demo {
test() {
console.log(100);

}
}

console.log(Demo);

控制台:

image-20240914171025917

关于构造类型

  1. 声明构造类型

    • new: 表示该类型可以使用new操作符调用。
    • …args:表示该类型可以接受任何数量的参数
    • any[]:表示该类型可以接受任意类型的参数。
    • {} :表示返回的是一对象类型(非null,非undefined)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //声明构造类型
    /*
    new: 表示该类型可以使用new操作符调用。
    ...args:表示该类型可以接受任何数量的参数。
    any[]:表示该类型可以接受任意类型的参数。
    {} :表示返回的是一对象类型(非null,非undefined)
    */
    type Constructor = new (...args:any[]) => {}
    //使用构造类型
    function test(fn:Constructor){
    console.log(fn);

    }
    class PersonTest{
    //必须要有静态属性wife
    static wife:string
    }
    //可以传递因为类符合上述类型条件
    test(PersonTest)
  2. 声明构造类型的同时还能规定静态属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type Constructor = {
    new (...args:any[]):{},
    wife:string
    }
    //使用构造类型
    function test(fn:Constructor){
    console.log(fn);

    }
    class PersonTest{
    //必须要有静态属性wife
    static wife:string
    }

这段代码的意思是

定义了一个类型别名 Constructor,它实际上是一个接口,描述了一个构造函数的要求。这个构造函数应该能够接受任意数量的参数(由 ...args:any[] 表示的 rest 参数),并且能够返回一个对象 {}。此外,还要求这个构造函数本身具有一个名为 wife 的字符串属性。

替换被装饰的类

可以看到,不仅获得原来类的属性,同时也新添加了一些新的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@logTime
class User{
constructor(public name:string,public age:number){}
speak(){
console.log('随便打印点什么');
}
}
interface User {
//声明一个函数,它的返回值是void
getCreateTime:()=> void
getCreateTime():void
}
//使用类装饰器来替换这个类,要求保留该类所有内容,并新添加一些内容
//构造类型
type Constructor = new (...args: any[]) => {}
function logTime<T extends Constructor>(target:T){
//这里返回User类
return class extends target {
//这里定义返回的新的类中独有的属性
createTime:Date
//使用构造器,来接住原本类中的属性
constructor(...args: any[]){
super(...args)
this.createTime = new Date();
}
getCreateTime(){
return `当前的时间是${this.createTime}`
}
}
}
const p5 = new User('张三',20);
console.log(p5.getCreateTime());

装饰器工厂

所谓的装饰器工厂:声明一个函数,该函数的返回值是一个装饰器,即装饰器工厂。该写法可以给装饰传递参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//装饰器工厂:声明一个函数,该函数的返回值是一个装饰器,即装饰器工厂。
@createDec(3)
class User {
name:string
age:number
constructor(name:string,age:number){
this.name = name,
this.age = age
}
speak(){
console.log('会说话');

}
}

//装饰器工厂 - 返回一个装饰器
function createDec(n:number){
return function (target:Function) {
//在类的原型对象上添加一个方法
target.prototype.introduce = function() {
for (let i = 0; i < n; i++) {
console.log(`你好,我叫${this.name},今年${this.age}岁`);
}
}
}
}
interface User {
introduce(): void
}
const p5 = new User('张三',20);
p5.introduce() //3次输出: 你好,我叫张三,今年20岁

装饰器组合

装饰器组合使用,执行顺序是: [由上到下] 执行装饰器工厂,获取到装饰器后,[由下到上]执行所有装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//装饰器
function test1(target:Function) {
console.log('test1')
}
//装饰器工厂
function test2() {
console.log( 'test2工厂')
return function (target:Function) {
console.log('test2')
}
}
//装饰器工厂
function test3() {
console.log( 'test3工厂')
return function (target:Function) {
console.log('test3')
}
}
//装饰器
function test4(target:Function) {
console.log('test4')
}


@test1
@test2()
@test3()
@test4
class Person5 {}

测试结果如下:

image-20240914205743860

属性装饰器

属性装饰器也是一个函数,他会收到两个参数,第一个参数会因为修饰的属性而改变,第二个参数是修饰的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function logProperty(target: object, propertyName: string) {
console.log(target);
console.log(propertyName);
}

class User {

@logProperty name: string;
@logProperty static age:number

constructor(name: string) {
this.name = name;
}
}

const user = new User('Alice');

说明:

  • 如果属性装饰器修饰的是实例属性,那么target这个参数指的是该类的原型对象。
  • 如果属性装饰器修饰的是静态属性,那么target这个参数指的是这个类本身

图示:

xxxxxxxxxx /* 约束屏幕尺寸 */@media screen and (max-width: 1024px) {  html {    font-size: 42px !important; }}@media screen and (min-width: 1920px) {  html {    font-size: 80px !important; }}css

关于属性遮蔽问题

简单说明一下这个问题:问题就是:代码执行顺序

  • 如果我们先进行实例化const p5 = new User('张三',20);那么在类的实例属性上就会存在age属性,那么这时去修改age属性是不会触发原型对象上的get和set函数的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User {

name: string;
age:number

constructor(name: string,age:number) {
this.name = name;
this.age = age
}
}
const p5 = new User('张三',20);
//要在原型对象上追加一个属性 age
let value:any;
Object.defineProperty(User.prototype,'age',{
get() {
return value
},
set(val) {
console.log(`新值是${val}`);
value = val

},
})

  • 如果我们先在类的原型对象上添加一个实例属性age的话,然后再实例化类,这时就会去调用构造器,执行this.age = age,但是实例上还没有age这个属性,接下来就沿着原型链去原型对象上查找,此时发现原型对象中存在age属性,且值为30,于是就把原型对象上的age属性交给了实例对象的age属性。那么实际上就相当于,当我们修改实例对象的age属性时,其实是修改的原型对象上的age属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User {

name: string;
age:number

constructor(name: string,age:number) {
this.name = name;
this.age = age
}
}

//要在原型对象上追加一个属性 age
let value = 30;
Object.defineProperty(User.prototype,'age',{
get() {
return value
},
set(val) {
console.log(`新值是${val}`);
value = val
},
})

const p5 = new User('张三',20);

属性装饰器的应用举例

场景:封装一个监视性的装饰器State来监视某个属性,当其发生变化时,打印变化的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

function State(target: object, propertyName: string) {
//我们就在其原型对象上追加一个age属性
let key = `_${propertyName}`;
Object.defineProperty(target,propertyName,{
get() {
return this[key]; //也是添加属性,只不过是通过键的方式来访问某个属性
},
set(newValue) {
console.log(`age属性被修改了,修改后的值是${newValue}`);
this[key] = newValue;
},
enumerable:true, //是否可枚举
configurable:true, //是否可配置
})
}

class User {

name: string;
@State age:number

constructor(name: string,age:number) {
this.name = name;
this.age = age
}
}

const p5 = new User('张三',20);
const p6 = new User('李三',20);

方法装饰器

方法装饰器依旧是一个函数,具体如下:

当我们装饰一个类的实例方法时:

  • target:是被装饰方法所在类的原型对象
  • propertyKey:被装饰的方法的方法名
  • descriptor:被装饰方法的描述

图示:

image-20240915103426810

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
console.log(target);
console.log(propertyKey);
console.log(descriptor);

}

//方法修饰器
class User {
constructor(public name:string,public age:number){}
@Demo
speak(){
console.log('说点什么');

}
static run(){
console.log(`我学会跑路了`);

}
}

当我们装饰一个类的静态方法时:

  • target:是被装饰方法所在的类。
  • propertyKey:被装饰的方法的方法名
  • descriptor:被装饰方法的描述

图示:

image-20240915103532506

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
console.log(target);
console.log(propertyKey);
console.log(descriptor);

}


//方法修饰器
class User {
constructor(public name:string,public age:number){}

speak(){
console.log('说点什么');

}
@Demo
static run(){
console.log(`我学会跑路了`);

}
}

方法装饰器的应用

能够实现:在方法调用前执行一些逻辑,在方法调用后执行一些逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
//1.保存原始方法
const originnal = descriptor.value;
//2.替换原始方法
descriptor.value = function(...args:any[]){
//函数执行前执行的逻辑
console.log(`${propertyKey}方法开始执行..........`);
//调用原始函数,注意千万不能丢了this的指向,这里推荐使用call或者apply来调用函数
originnal.call(this,...args)
//函数执行后执行的逻辑
console.log(`${propertyKey}方法结束执行..........`);
}

}

//方法修饰器
class User {
constructor(public name:string,public age:number){}
@Demo
speak(){
console.log('说点什么');

}

static run(){
console.log(`我学会跑路了`);

}
}
const p5 = new User('张三',20)
p5.speak()

控制台输出

image-20240915104747430

访问器装饰器

访问器装饰器是装饰类中的get和set方法的一种装饰器。

参数说明:

  • target:

    • 对于实例中的访问器来说,target指的是该访问器所属的原型对象。
    • 对于静态访问器来说,target指的是该访问器所属的类
  • propertyKey:属性访问器的名称

  • descriptor:属性访问器的描述。

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor){
console.log(target);
console.log(propertyKey);
console.log(descriptor);

}

class User {
constructor(public name:string,public _age:number){}
speak(){
console.log('说点什么');

}
static run(){
console.log(`我学会跑路了`);
}
@Demo
get age(){
return this._age
}
set age(val){
this.age = val
}

}

访问器装饰器举例

给set加一个限制,表示我们不能随意设置任意值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//为了能传递参数,这里我们使用装饰器工厂
function Demo(min:number,max:number){
return function(target:object,propertyKey:string,descriptor:PropertyDescriptor){
//保存原来类中的set
const originnal = descriptor.set;
//给现在的set换新逻辑
descriptor.set = function(value:number){
if(value > max || value < min){
console.log(`您设置的值应该在${min}${max}`);
throw new Error('您设置的数值不在范围内')
}
//如果有set,那么我们调用set。
if(originnal) originnal.call(this,value)
}
}

}
//方法修饰器
class User {
constructor(public name:string,public _temp:number){}


get temp(){
return this._temp
}
@Demo(-50,50)
set temp(val){
this._temp = val
}
}
const p5 = new User('小李',90)
p5.temp = -100;
console.log(p5._temp); //您设置的数值不在范围内

参数装饰器

在 TypeScript 中,参数装饰器是一种特殊类型的装饰器,它们应用于类的方法或构造函数的参数声明。当在方法或构造函数的参数前使用装饰器时,这些装饰器即为参数装饰器。参数装饰器接收三个参数:

  1. 对于类的方法来说,第一个参数是 target(原型对象)。
  2. 第二个参数是方法的 propertyKey(即方法名)。
  3. 第三个参数是索引签名,表示被装饰的参数的位置(从0开始计数)。

下面是一个简单的例子,展示了如何使用参数装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 参数装饰器
function required(target: any, propertyKey: string | symbol, parameterIndex: number) {
console.log(`Called with target: ${target}, propertyKey: ${propertyKey}, parameterIndex: ${parameterIndex}`);
}

class Greeter {
greet(@required name: string) {
console.log("Hello, " + name + "!");
}
}

let greeter = new Greeter();
greeter.greet("World");

在这个例子中,@required 被用作 greet 方法中 name 参数的装饰器。每次创建 Greeter 实例并调用其 greet 方法时,required 装饰器就会被调用,并打印出装饰器接收到的信息。

image-20240915114010933