TypeScript简介

  1. TypeScript由微软开发,是基于JavaScript的一个扩展语言
  2. TypeScript包含了JavaScript的所有内容
  3. TypeScript增加了静态类型检查、接口、泛型等很多现代开发特性,更适合大型项目的开发
  4. TypeScript需要编译为JavaScript,然后交给浏览器或其他JavaScript运行环境执行

JavaScript中的典型问题

  1. 容易混淆的数据类型
  2. 逻辑漏洞
  3. 访问不存在的属性
  4. 低级的拼写错误

静态类型检查:

  • 在代码运行前进行检查,发现代码的错误或不合理之处,减少运行时异常的出现的机率,此种检查叫做静态类型检查,TypeScript的核心就是这个。简而言之就是把运行时的错误前置
  • 同样的功能,TypeScript的代码量要大于JavaScript,但由于TypeScript的代码结构更加清晰,在后期代码的维护中TypeScript远胜于JavaScript

编译TS

命令行编译(不推荐)

要把.ts文件编译成.js文件,需要配置TypeScript的编译环境,步骤如下:

第一步:

创建一个demo.ts

1
2
3
4
5
const person = {
name: '张三',
age: 18
}
console.log(`我叫${person.name},我今年${person.age}岁了`)

第二步:

安装全局TypeScript

1
npm i typescript -g

第三步:

使用命令编译.ts文件

1
tsc demo.ts

自动化编译

第一步:

创建TypeScript编译控制文件

1
tsc --init
  1. 工程中会生成一个tsconfig.json配置文件,其中包含着很多编译时的配置
  2. 默认编译的JS版本是ES7,可以手动调整为其他版本

第二步:

监视目录中的.ts文件变化

1
tsc --watch

第三步:

小优化,当编译出错时不生成.js文件

1
tsc --noEmitOnError --watch

Tips:在tsconfig.json文件中也可以修改

在Vue或其他框架中,TypeScript不许要用户自己处理,框架会自动编译

类型声明

格式:

1
2
3
4
修饰符 变量名:数据类型 = 值;
function 函数名(形参名:数据类型,形参名:数据类型):返回值数据类型 {
...
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let a:string;
let b:boolean;
let c:number;


a = "Hello World";
b = false;
c = 2024;

function count(x:number,y:number):number {
return x + y;
}

let res:number = count(1,2);

console.log(a,b,c);

console.log(res);

特殊方法(不常用)

1
2
3
4
let a:"你好";	// a的值只能为"你好"

a = "世界"; // 报错!!
a = "你好"; // 不报错

类型推断

TS会根据代码进行类型推断,如下所示:

1
2
let a = 2024;
a = "你好"; // 报错:不能将String类型分配给Number类型

但是不推荐省略数据类型

类型总览

JS中的数据类型:

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

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

TS中的数据类型:

  1. 上述所有的JS数据类型
  2. 六个新类型:
    1. any
    2. unknown
    3. never
    4. void
    5. tuple
    6. enum
  3. 两个用于自定义类型的方式:
    1. type
    2. interface

注意点:

在 JavaScript中的这些内置构造函数:Number、String、Boolean,它们用于创建对应的包装对象,在开发日常很少使用,在TypeScript中也是同理,所以在TypeScript中进行类型声明时,通常都是小写的number、string、boolean

示例:

1
2
3
4
5
6
7
let str1:string;
str1 = "Hello";
str2 = new String("Hello"); // 报错

let str2:String;
str2 = "Hello";
str2 = new String("Hello")

原始类型VS包装对象:

  • 原始类型:如number、string、boolean,在JavaScript中是简单数据类型,它们在内存中占用空间少,处理速度快
  • 包装对象:如Number对象、String对象、Boolean对象,是复杂类型,在内存中占用更多空间,实际开发中很少使用
  • 自动装箱:JavaScript在必要时会自动将原始类型包装成对象,以便调用方法或访问属性

常用类型

点击此处查看数据类型

any

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 明确的表示any类型(显式)
let a:any;

a = 123;
a = "hello world"
a = false;
// 以上均无报错

// 隐式
let b;
b = 100;
b = "bye bye";
b = false
// 以上均无报错

注意:any类型的变量,可以赋值给任意类型的变量

1
2
3
4
5
let c:any;
c = 5;

let x:string;
x = c; // 不会报错,且x的值为5

unknown

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

1
2
3
4
5
let c:unknown;
c = 5;

let x:string;
x = c; // 报错

并且,如果代码为以下形式

1
2
3
4
5
let c:unknown;
c = "hello world";

let x:string;
x = c; // 依然会报错

解决办法:

1
2
3
4
// 第一种
if (typeof c === "string") {
x = c;
}
1
2
3
// 第二种(断言)
x = a as string // 断言方式1
x = <string>a // 断言方式2

读取any类型数据的任何属性都不会报错,而unknown则相反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let str1:string;
str1 = 'hello';
str1.toUpperCase(); // 无警告

let str2:any;
str2 = 'hello';
str2.toUpperCase(); // 无警告

let str3:unknown;
str3 = 'hello';
str3.toUpperCase(); // 警告:"str3"的类型为"未知"

// 使用断言强制指定str3的类型为string
(str3 as string).toUpperCase(); // 无警告

never(少见)

任何值都不是,简言之就是不能有值,undefined、null、’’、0都不行

  1. 几乎不用never去直接限制变量,因为没有意义
1
2
3
4
5
6
7
8
let a:never;

// 以下都会报错
a = 1;
a = false;
a = "hello";
a = undefined;
a = null;
  1. never一般是TypeScript主动推断出来的
1
2
3
4
5
6
7
8
let a:string;
a = "hello";

if (typeof a === "string") {
console.log(a.toUpperCase);
} else {
console.log(a); // TypeScript会推断出此处的a是never,因为没有任何一个值符合此处逻辑
}
  1. never也可以用于限制函数的返回值
1
2
3
function throwError(str:string):never {
throw new Error("程序异常" + str);
}

void

  1. void通常用于函数返回值的声明(就和Java中的void一样)
1
2
3
4
5
function logMessage(str:string):void {
console.log(str);
}

logMessage("你好");

注意:即使没有写函数的返回值,但是还是会有一个隐式返回值undefined

  1. 用void限定的函数还可以是以下的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
function logMessage1(str:string):void {
console.log(str);
}

function logMessage2(str:string):void {
console.log(str);
return;
}

function logMessage3(str:string):void {
console.log(str);
return undefined;
}
  1. void函数不能对其返回值进行操作(这是与undefined函数的区别所在)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test1(str:string):void {
console.log("Hello");
return undefined;
}

function test2(str:string):undefined{
return undefined;
}

let res1 = test1();
let res2 = test2();

if (res1) { // 报错
console.log(res1);
}

if (res2) {

console.log(res2);
}

object

关于objectObject,实际开发中用的相对较小,因为范围太大了

声明对象类型

object(小写)

所有非原始类型,可以存储:对象、函数、数组等,由于限制的范围比较宽泛,实际开发中使用的相对较小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a:object;	// a的值可以是任何非原始类型,包括对象、函数、数组

// 以下均无问题
a = {};
a = {name:"张三"};
a = [1,2,3];
a = function() {};
a = new String("Hello");
class Person {};
a = new Person{};

// 原始类型警告:
a = 1;
a = true;
a = "Hello";
a = null;
a = undefined;

Object(大写)(少见)

官方描述:所有可以调用Object方法的类型

简而言之:除了undefined和null的任何值

1
2
3
4
5
6
7
8
9
10
11
12
13
let b:Object;

// 以下均无问题
b = {};
b = {name:"张三"};
b = [1,2,3];
b = function() {};
b = new String("Hello");
class Person {};
b = new Person{};
b = 1;
b = false;
b = "Hello";

限制一般对象

实际开发中,限制一般对象,通常用以下形式:

1
2
3
4
// 限制person对象必须有name属性,age为可选属性
let person = {name:string, age?:number};

person = {name:"张三",age:18};

索引签名

允许定义对象可以具有任意数量的属性(具有动态属性的对象)

格式:[键名:键数据类型]:值数据类型

1
2
3
let person = {name:string, age?:number, [key:string]:any};

person = {name:"tom",age:18,gender:"男",city:"上海"};

声明函数类型

格式:

1
2
3
4
5
let 函数名:(形参名:形参类型,形参名:形参类型) => 返回值类型

let 函数名 = function (形参名,形参名) {
return 返回值类型数据;
}

示例:

1
2
3
4
5
let count:(a:number,b:number) => number;

let count = (x,y) => {
return x + y;
}

注意:

  1. TypeScript中的=>在函数类型声明时表示函数类型,描述其参数类型和返回类型(分隔符)
  2. JavaScript中的=>时一种定义函数的语法,是具体的函数实现
  3. 函数类型声明还可以使用:接口、自定义类型等方式

声明数组类型

格式:

1
2
3
4
5
// 方式1
let 数组名:数据类型[];

// 方式2
let 数组名:Array<数据类型>;

示例:

1
2
3
4
5
let arr1:string[];
let arr2:Array<number>;

arr1 = ["a","b"];
arr2 = [100,200];

tuple

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 第一个元素必须是string类型,第二个元素必须是number类型
let arr1:[string,number];
// 第一个元素必须是number类型,第二个元素是可选的,如果存在,必须是boolean类型
let arr2:[number,boolean?];
// 第一个元素必须是boolean类型,后面的元素可以是任意数量,但类型必须是string类型
let arr3:[boolean,...string[]];

// 赋值
arr1 = ["Hello",123];
arr2 = [100,false];
arr2 = [200];
arr3 = [true,"Hello","World"];
arr3 = [false];

enum

枚举(enum)可以定义一组命名常量,它能增强代码的可读性,也让代码更好维护

如下代码的功能是:

根据调用walk时传入的不同参数,执行不同的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function walk(str:string) {
if (str === "up") {
console.log("向上走");
} else if (str === "down") {
console.log("向下走");
} else if (str === "left") {
console.log("向左走");
} else if (str === "right") {
console.log("向右走");
} else {
console.log("未知方向");
}
}

walk("up");
walk("down");
walk("left");
walk("right");

存在的问题是调用walk时传参时没有任何提示,编码者很容易写错字符串内容;并且用于判断逻辑的up、down、left、right是连续且相关的一组值,那此时就特别适合使用枚举(enum)

数字枚举

数字枚举是最常见的枚举类型,其成员的值会自动递增,且数字枚举还具备反射的特点。

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

console.log(Direction);
// {0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right', Up: 0, Down: 1, Left: 2, Right: 3}

根据以上特点,可以修改之前的代码

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
enum Direction {
Up,
Down,
Left,
Right
}

function walk(data:Direction) {
if (data === Direction.Up) {
console.log("向上走");
} else if (data === Direction.Down) {
console.log("向下走");
} else if (data === Direction.Left) {
console.log("向左走");
} else if (data === Direction.Right) {
console.log("向右走");
} else {
console.log("未知方向");
}
}

walk(Direction.Down);
walk(Direction.Up);
walk(Direction.Left);
walk(Direction.Right);

优化过后的代码更加的便于维护,也可以防止错误输入的发生

字符串枚举

枚举成员是字符换

1
2
3
4
5
6
7
8
9
enum Direction {
Up = "up",
Down = "down",
Left = "left",
Right = "right"
}

console.log(Direction);
// {Up: 'up', Down: 'down', Left: 'left', Right: 'right'}

常量枚举

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

内联:TypeScript在编译时,会将枚举成员引用替换为它们的实际值,而不是生成额外的枚举对象,这可以减少生成的JS代码量,并提高运行时的性能

例如:

普通枚举下的TS:

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

console.log(Direction.Up); // 返回值:0

普通枚举下生成的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 = {}));
console.log(Direction.Up); // 返回值:0

常量枚举下的TS:

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

console.log(Direction.Up); // 返回值:0

常量枚举下生成的JS:

1
2
"use strict";
console.log(0 /* Direction.Up */); // 返回值:0

type

type可以为任意类型创建别名,让代码更简洁、可读性更强,同时更方便地进行类型复用和扩展

基本用法

类型别名使用type关键字定义,type后跟类型名称,如下:

1
2
3
4
type num = number;

let price:num;
price = 100;

联合类型

联合类型是一种高级形式,它表示一个值可以是几种不同类型之一

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Statu = number | string
type Gender = "男" | "女"

function printStatus(data:Status):void {
console.log(data);
}

function printGender(data:Gender) {
console.log(data);
}

printStatus(404);
printStatus("404");
printStatus(false); // 错误

printGender("男");
printGender("女");
printGender("未知"); // 错误

交叉类型

交叉类型(Intersection Types)允许将多个类型合并为一个类型。合并后的类型将拥有所有被合并类型的成员。交叉类型通常用于对象类型

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 面积
type Area = {
width:number;
height:height;
}

// 地址
type Address = {
num:number;
cell:number;
room:string;
}

type House = Area & Address;

// 保证Area和Address的每个属性都存在
const house:House = {
height:100,
width:100,
num:101,
cell:2,
room:"205"
}

特殊情况(void不奏效)

在函数定义时,限制函数返回值为void,那么函数的返回值就必须为空

1
2
3
4
5
function demo():void {
return undefined; // 正确

return null; // 错误
}

使用类型声明限制函数返回值为void,TypeScript并不会严格要求函数返回空

1
2
3
4
5
6
7
8
9
10
11
type LogFunc = () => void;

const f1:LogFunc = () => {
return 100; // 不会报错
}

const f2:LogFunc = () => 200; // 不会报错

const f3:LogFunc = function() {
return 300; // 不会报错
}

导致的原因:

是为了确保如下代码成立,由于Arrary.prototype.push的返回为一个数字,而Arrary.prototype.forEach方法期望其回调的返回类型是void

1
2
3
4
const src = [1,2,3];
const dst = [0];

src.forEach((el) => {dst.push(el)});

类(Class)

TypeScript中的类和Java的类相似

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}岁了`)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Student extends Person{
grade:string;

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

study() {
console.log(`${this.name}正在学习中`);
}

override speak() {
console.log(`我叫:${this.name},今年${this.age}岁了,正在上${this.grade}`)
}
}

上述代码包含了:成员变量声明,构造函数声明,成员方法声明,成员变量继承,成员方法重写

属性修饰符

和Java类似,TypeScript中也有属性修饰符

修饰符 含义 具体规则
public(默认) 公共属性 可以被:类内部、子类、类外部访问
protected 受保护属性 可以被:类内部、子类访问
private 私有属性 可以被:类内部访问
readonly 只读属性 属性无法修改
1
2
3
4
5
6
7
class Person {
public name:string;
protected age:number;
private gender:string;

...
}

属性简写(成员变量简写)

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

抽象类

概念和Java中的类似(都是面向对象编程OOP):

  • 抽象类不能被实例化(new),其意义是可以被继承,抽象类中可以有普通方法,也可以有抽象方法。

  • 有抽象方法的类一定是抽象类。

  • 继承抽象类的实例必须要实现抽象类中的方法

格式:

抽象类定义:

1
abstract class 抽象类名 {}

抽象类方法定义:

1
abstract 方法名:返回值类型;

抽象类的实现:

1
class 类名 extends 抽象类 {}

实例:

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
abstract class Package {
// 构造函数
constructor(public weight:number) {};

// 抽象方法
abstract calculate():number;

// 具体方法
printInfo() {
console.log(`包裹重量为${this.weight}kg,运费为${calculate()}元`);
}
}

class StandardPackage extends Package {
// 构造函数
constructor(weight:number,public unitPrice:number) {super(weight)};

// 实现抽象方法
calculate():number {
return this.weight * this.unitPrice;
}
}

const s1 = new StandardPackage(20,15);
s1.printInfo();

接口(Interface)

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

格式:

接口定义:

1
interface 接口名 {}

类接口实现:

1
class 类名 implements 接口名 {}

对象接口实现(很像之前学的type):

1
修饰符 对象名:接口名 = {}

函数接口实现:

1
修饰符 函数名:接口名 = (形参) => {...}

实例:

类接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface PersonInterface {
name:string;
age:number;
speak(n:number):void;
}

class Person implements PersonInterface {
constructor(public name:string,public age:number) {};
speak(n:number) {
for (let i=0;i < n;i++) {
console.log(`我叫:${this.name},今年${this.age}岁了`);
}
}
}

const p1 = new Person("张三",18);
p1.speak(4);

对象接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface UserInterface {
name:string;
readonly gender:string;
age?:number;

run:(n:number) => void;
}

const user:UserInterface = {
name:"张三",
gender:"男",
age:18,
run(n): {
console.log(`跑了${n}米`)
}
}

函数接口实现

1
2
3
4
5
6
7
interface CountInterface {
(a:number,b:number):numbner;
}

const count:CountInterface = (x,y) => {
return x + y;
}

接口继承

格式:

1
2
3
4
5
6
7
interface 接口1 {
...
};

interface 接口2 extends 接口1 {
...
}

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface PersonInterface {
name:string;
age:number;
}

interface StudentInterface extends PersonInterface {
grade:string;
}

const s1:StudentInterface = {
name:'张三',
age:18,
grade:'大二'
}

接口合并

格式:

1
2
3
4
5
6
7
8
interface 接口1 {
属性1:属性类型;
属性2:属性类型;
}

interface 接口1 {
属性3:属性类型;
}

最后,接口1中会有属性1、2、3

interface和type的区别

相同点:

  1. interface和type都可以用于定义对象结构,两者在许多场景中是可以互换的

不同点:

  1. interface:更专注于定义对象和类的结构,支持继承、合并
  2. type:可以定义类型别名、联合类型、交叉类型,但不支持继承和自动合并

泛型

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

示例:

1
2
3
4
5
6
7
function logMessage<T>(data:T):T {
console.log(data);
return data;
}

logMessage<number>(100);
logMessage<string>("Hello");

多个泛型:

1
2
3
4
5
6
7
8
function logMessage<T,U>(data1: T,data2: U): T | U {
console.log(data1,data2);
return Date.now() % 2 ? data1 : data2;
}

console.log(logMessage<number,boolean>(100,false));

console.log(logMessage<string,string>("Hello","World"));

接口泛型:

1
2
3
4
5
6
7
8
9
10
11
interface PersonInterface<T> {
name:string;
age:number;
extraInfo:T;
}

const p1:PersonInterface<number> {
name:"张三",
age:16,
extraInfo:100
}

类泛型:

1
2
3
4
5
6
7
8
9
10
class Person<T> {
constructor(public name:string,public age:string,public extraInfo:T);

speak() {
console.log(`我叫${this.name},今年${this.age}岁了`);
console.log(extraInfo);
}
}

let p1 = new Person<string>("张三",15,"吃饭")

类型定义文件(.d.ts文件)

类型声明文件是TypeScript中的一种特殊文件,通常以.d.ts作为拓展名。它的主要作用是为现有的JavaScript代码提供类型信息,使得TypeScript能够在使用这些JavaScript库或模块时进行类型检查和提示

示例:

1
2
3
4
5
6
7
8
// demo.js
export function add(a,b) {
return a+b;
}

export function mul(a,b) {
return a*b;
}
1
2
3
4
5
// demo.d.ts
declare function add(a:number,b:number):number;
declare function mul(a:number,b:number):number;

export {add,mul};
1
2
3
4
5
6
7
// demo.ts
import {add,mul} from "./demo.js";

const x = add(2,3);
const y = mul(4,5);

console.log(x,y)

装饰器

  1. 装饰器本质是一种特殊的函数,它可以对:类、属性、方法、参数进行拓展,同时能让代码更简洁
  2. 目前,装饰器依然是实验性特性,需要开发者手动调整配置,来开启装饰器支持
  3. 装饰器共有5种:
    1. 类装饰器
    2. 属性装饰器
    3. 方法装饰器
    4. 访问器装饰器
    5. 参数装饰器

Tips:虽然TypeScript5.0中可以直接使用类装饰器,但是为了确保其他装饰器可用,现阶段使用时,仍建议使用experimentalDecorators配置来开启装饰器支持,而且不排除在未来的版本中,官方会进一步调整装饰器的相关语法

类装饰器

基本语法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
Demo函数会在Person类定义时执行
参数说明:
target参数是被装饰的类,即Person
*/
function Demo(target:Function) {
console.log(target); // 输出Person这个类
}

@Demo
class Person {
constructor(public name:string,public age:number) {};
}

引用举例

需求:定义一个装饰器,实现Person实例调用toString时返回JSON.stringify的执行结果

没有使用装饰器的代码:

1
2
3
4
5
6
7
class Person {
constructor(public name:string,public age:number) {};
}

let p1 = new Person("张三",18);
console.log(p1.toString()); // 返回值为[object Object]
console.log(JSON.stringify(p1)); // 返回值为:{"name":"张三","age":18}

使用装饰器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function CustomString(target:Function) {
target.prototype.toString = function() {
return JSON.stringify(this);
}
}

@CustomString
class Person {
constructor(public name:string,public age:number) {};
}

let p1 = new Person("张三",18);
console.log(p1.toString()); // {"name":"张三","age":18}

返回值

类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类

类装饰器无返回值:若类装饰器无返回值或返回undefined,那被装饰的类不会被替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function demo() {
return class {
test() {
console.log(200);
console.log(300);
console.log(400);
}
}
}

@demo
class Person {
test() {
console.log(100);
}
}

console.log(Person); // 返回值为:test类

构造类型(自定义类型)

在TypeScript中,Function类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等,但并非所有Function类型的函数都可以被new关键字实例化,例如箭头函数是不能被实例化的,那么TypeScript中该如何声明一个构造类型呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
new 表示:该类型是可以用new操作符调用的
...args 表示:构造器可以接收任意数量的参数
any[] 表示:构造器可以接收任意类型的参数
{} 表示:返回类型是对象(非null、非undefined对象)
*/


type Constructor = new (...args:any[]) => {};

function test(fn:Constructor) {};
class Person{};
test(Person);
1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个构造类型,且包含一个静态属性
type Constructor = {
new(...args: any): {};
el:string;
}

function test(fn:Constructor){};
class Person {
static el:string;
}

test(Person)

替换被装饰的类

对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态

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
// 需求:设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间
type Constructor = new (...args:any[]) => {}; // 创建类型约束

function LogTime<T extends Constructor>(target:T) {
return class extends target { // 返回一个类,类继承自target(被修饰的类)
createdTime:Date; // 添加一个类属性,用于记录时间
constructor(...args:any[]){
super(args);
this.createdTime = new Date(); // 将创建类的时间保存
}
getTime(){
return `该对象创建于${this.createdTime}`; // 用于打印结果
}
}
}

@LogTime
class Person {
constructor(public name:string,public age:number) {};
speak() {
console.log(`我叫${this.name},今年${this.age}岁了`);
}
}

const p1 = new Person("张三",16);
console.log(p1);
// @ts-ignore // 取消检查
console.log(p1.getTime());

装饰器工厂

修饰器工程是一个返回修饰器函数的函数,可以为修饰器添加参数,可以更灵活地控制装饰器的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 需求:定义一个LogInfo类装饰器工厂,实现Person实例可以调用到introduce方法,且introduce中输出内容的次数,由LogInfo接收的参数决定
// 需求:设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间
type Constructor = new (...args:any[]) => {};

function LogInfo(n:number) {
return function(target:Constructor) {
target.prototype.introduce = function() {
for (let i = 0; i < n; i++) {
console.log(`我叫${this.name},我今年${this.age}岁了`);
}
}
}
}

@LogInfo(5)
class Person {
constructor(public name:string,public age:number) {};
}

let p1 = new Person("张三",12);
// @ts-ignore
p1.introduce();

装饰器组合

装饰器可以组合使用,执行顺序为:先由上到下的执行所有的装饰器工厂,依次获取到装饰器,然后再由下到上执行所有的装饰器

属性装饰器

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
参数说明:
target:对于静态属性来说是类,对于实例属性来说是类的原型对象
propertyKey:属性名
*/
function demo(target:object,propertyKey:string) {
console.log(target);
console.log(propertyKey);
}


class Person {
@demo public name:string;
public age:number;
@demo static school:string;
constructor(name:string,age:number) {
this.name = name;
this.age = age;
};
}

属性遮蔽

如下所示:

代码1:

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
class Person {
public name:string;
public age:number;

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

let p1 = new Person("张三",18);

let value = 99;
Object.defineProperty(Person.prototype,'age',{

get() {
return value;
},

set(val) {
value = val;
}

})

console.log(p1.age); // 18
console.log(Person.prototype.age); // 99

代码2:

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
class Person {
public name:string;
public age:number;

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


let value = 99;
Object.defineProperty(Person.prototype,'age',{

get() {
return value;
},

set(val) {
value = val;
}

})

let p1 = new Person("张三",18);


console.log(p1.age); // 18
console.log(Person.prototype.age); // 18

代码1和代码2输出结果的不同就是由于属性遮蔽导致的

应用场景

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
// 需求:实例对象的属性被修改后,会有提示
function State(target:object,propertyKey:string) {
let key = `__${propertyKey}` // 保证实例的属性之间互相独立
Object.defineProperty(target,propertyKey,{
get() {
return this[key];
},
set(newValue) {
console.log(`${propertyKey}的值被修改了,最新的值为${newValue}`);
this[key] = newValue
},
})
}

class Person {
public name:string;
@State public age:number;

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


const p1 = new Person("张三",18); // age的值被修改了,最新的值为18
p1.age = 19; // age的值被修改了,最新的值为19

方法装饰器

基本语法

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
/* 
参数说明:
target:对于静态方法来说值是类,对于实例方法来说值是原型对象
propertyKey:方法的名称
descriptor:方法的描述对象,其中value属性是被装饰的方法
*/
function Demo(target:object,propertyKey:string,descriptor:PropertyDescriptor) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}

class Person {
constructor(public name:string,public age:number) {};

// Demo修饰实例方法
@Demo speak() {
console.log(`我叫:${this.name},今年${this.age}岁了`);
}

// Demo修饰静态方法
@Demo static isAdult(age:number) {
return age>= 18;
}
}

获取方法本身或修改方法本身

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) {
// 存储原始方法
const originnal = descriptor.value;

// 修改原始方法
descriptor.value = () => {
console.log("修改了原始方法");
}
}

class Person {
constructor(public name:string,public age:number) {};

// Demo修饰实例方法
@Demo speak() {
console.log(`我叫:${this.name},今年${this.age}岁了`);
}

// Demo修饰静态方法
@Demo static isAdult(age:number) {
return age>= 18;
}
}

const p1 = new Person("张三",18);
p1.speak(); // 控制台:修改了原始方法