TS - 概述

  1. TypeScript 是 JavaScript 的超集
  2. 它对 JS 进行了扩展,向 JS 中引入了类型的概念,并添加了许多新的特性。
  3. TS 代码需要通过编译器编译为 JS,然后再交由 JS 解析器执行。
  4. TS 完全兼容 JS,换言之,任何的 JS 代码都可以直接当成 TS 使用。

静态类型检查

  • 静态类型检查,即在代码运行前进行检查,发现代码的错误或不合理之处,减少运行时异常的出现几率。
  • TypeScript 的核心就是静态类型检查,即将运行时的错误前置。

TS - 编译

浏览器无法直接运行 .ts 文件,因此需要将 .ts 文件编译为 .js 文件。

编译 - 命令行

Step1. npm install -g typescript

Step2. tsc <.js 文件路径>

tsc 是全局安装 typescript 后暴露的命令,是 typescript compiler 的简写,用于将 .ts 文件编译为 .js 文件

编译 - 自动化

Step1. npm install -g typescript

Step2. tsc --init

创建了 tsconfig.json 配置文件,规定了将 .ts 文件编译为 .js 文件的方式。可以在配置文件中设置 "noEmitOnError": true,使得编译出错时不生成 .js 文件。

Step3. tsc --watch

监视当前目录下的所有 .ts 文件的变化,并根据 tsconfig.json 将其自动编译为 .js 文件。--watch 后也可以加具体的文件路径,表示只监视特定的文件。

TS - 类型声明

  1. 解释:类型声明给变量设置了类型,使得变量只能存储某种类型的值。

  2. 语法

    1
    2
    3
    4
    5
    6
    7
    let 变量: 类型;

    let 变量: 类型 = 值;

    function fn(参数: 类型, 参数: 类型): 类型{
    ...
    }

TS - 类型推断

解释:当对变量的声明和赋值是同时进行的,TS 编译器会自动判断变量的类型

JS|TS - 类型总览

  1. JavaScript 中的数据类型:①string ②number ③boolean ④null ⑤undefined ⑥bigint ⑦symbol ⑧object
  2. TypeScript 中的数据类型
    1. JS 所有类型
    2. TS 新增类型:①any ②unknown ③never ④void ⑤tuple ⑥enum ⑦字面量
    3. TS 用于自定义类型的关键字:①type ②interface
  3. 注意事项:String 和 string 都是 TypeScript 中所接受的类型,不过前者是包装对象类型,一般不建议使用;后者是原始类型,推荐使用
    1. 原始类型:内存占用空间小,处理速度快。
    2. 包装对象:内存占用空间多,但可以调用方法或访问属性。

TS - 常用类型

any

  1. 解释:任意类型,即一旦将变量类型限制为 any,那么就意味着放弃对该变量的类型检查。
    1
    2
    3
    4
    5
    6
    7
    8
    // 显示 any
    let a: any
    // 隐式 any
    let b
    // 可以给 any 类型的变量赋任何值,不会警告
    b = 100
    b = 'hello'
    b = false
  2. 注意:any 类型的变量,可以赋值给任意类型的变量,从而造成污染
    1
    2
    3
    4
    5
    let a: any
    a = 100

    let b: string
    b = a // 无警告

unknown

  1. 解释:未知类型,可以理解为类型安全any。该类型会强制开发者在使用之前对变量进行类型检查(if 判断类型断言),否则会进行警告。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let a: unknown
    a = 100
    a = 'hello'
    a = false

    let x: string
    x = a // 警告:不能将类型 unknown 分配给类型 string

    // 方式一:if 判断,让 ts 知道 unknown 变量的具体类型
    if(typeof a === 'string'){
    x = a
    }

    // 方式二:类型断言,用于指定 unknown 变量的具体类型
    x = a as string // 类型断言写法 1
    x = <string>a // 类型断言写法 2
  2. 注意:读取 any 类型变量的任何属性都不会报错,而 unknown 正相反。

never

解释:不是任何类型,即 never 类型的变量不能有值。

  • 不用 never 去限制变量,无意义。
  • TypeScript 会根据代码执行情况,推断出某些永远无法执行的变量类型为 never
    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) // 这里的 a 会被推断为 never 类型,因为没有值符合这里的逻辑
    }
  • never 也可以用于限制函数的返回值类型,当 ①程序无法正常执行(报错)②程序永远无法停止(死循环)时,其返回值类型就可以是 never
    1
    2
    3
    function throwError(message: string): never {
    throw new Error(message)
    }

void

  1. 解释:空类型,常用于函数返回值的类型声明,表示 ①函数不返回任何值,②调用者也不应该依赖其返回值进行任何操作。其中,undefinedvoid 唯一接受的一种返回值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 隐式返回 undefined - 1(无警告)
    function print(msg: string): void{
    console.log(msg)
    }

    // 隐式返回 undefined - 2(无警告)
    function print(msg: string): void{
    console.log(msg)
    return
    }

    // 显示返回 undefined - 3(无警告)
    function print(msg: string): void{
    console.log(msg)
    return undefined
    }

    let res = print("hello world")
    if(res) console.log("可以获取函数的 void 返回值吗?") // 警告
  2. 注意:如果函数的返回值被声明为 void,那么
    • 语法上,函数可以返回 undefined,显式还是隐式返回无所谓。
    • 语义上,函数调用者不应该关心函数返回的值,也不应依赖返回值进行任何操作。

object

  1. object:所有非原始类型,包括对象、函数、数组等。
  2. Object:所有可以调用 Object 方法的类型除了 undefined 和 null 的任何值)。
  3. 注意:object 和 Object 在实际开发中使用频率极低。

声明对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明(一般)对象类型的必选属性
let person: {name: string, age: number} // 属性以 , 分隔
let person: {name: string; age: number} // 属性以 ; 分隔
let person: { // 属性以换行分隔
name: string
age: number
}

// 声明(一般)对象类型的可选属性
let person: { name: string, age: number, gender?: string }

// 声明(一般)对象类型的任意数量的类型不定的属性(动态属性)
let person: {
name: string
age: number
[key: string]: any
}

声明函数类型

1
2
// 声明函数类型,包括参数及返回值
let sum: (a: number, b: number) => number

声明数组类型

1
2
3
4
5
// 声明数组类型 - 方式 1(类型[])
let arr: string[]

// 声明数组类型 - 方式 2(Array<类型>)
let arr: Array<string>

tuple

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

1
2
3
4
5
6
// 两个元素,第一个必须是 string 类型,第二个必须是 number 类型
let arr1: [string, number]
// 第一个元素必须是 number 类型,第二个元素是可选的,如果存在,必须是 boolean 类型
let arr2: [number, boolean?]
// 第一个元素必须是 number 类型,后边可以是任意数量的 string 类型的元素
let arr3: [number, ...string[]]

enum

  1. 解释:enum 可以定义一组命名常量,用于增强代码的可读性和可维护性。枚举分为数字枚举、字符串枚举、常量枚举

  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
    enum Sex {
    Male,
    Female
    }
    /*
    Sex 被编译为,
    var Sex;
    (function (Sex) {
    Sex[Sex["Male"] = 0] = "Male";
    Sex[Sex["Female"] = 1] = "Female";
    })(Sex || (Sex = {}));
    */

    function recruitPolicy(sex: Sex): void {
    if (sex === Sex.Male) {
    console.log('height >= 185cm and weight <= 80kg');
    } else if (sex === Sex.Female) {
    console.log('height >= 165cm and weight <= 70kg');
    } else {
    console.log('invalid params');
    return;
    }
    console.log('eyesight >= 3.8');
    console.log('salary = 3000');
    }

    recruitPolicy(Sex.Female);
  3. 字符串枚举:枚举成员的值是字符串,不存在反向映射。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    enum Sex {
    Male = 'male',
    Female = 'female'
    }

    /*
    Sex 被编译为,
    var Sex;
    (function (Sex) {
    Sex["Male"] = "male";
    Sex["Female"] = "female";
    })(Sex || (Sex = {}));
    */
  4. 常量枚举:特殊的枚举类型,使用 const enum 关键字定义,在编译时会被内联,避免生成额外的代码。

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

    1
    2
    3
    4
    5
    6
    const enum Sex {
    Male = 'male',
    Female = 'female'
    }

    console.log(Sex.Male)

    被编译为,

    1
    2
    "use strict";
    console.log("male" /* Sex.Male */);

type

  1. 解释:type 关键字用于为任意类型创建别名
  2. 基本使用:type 别名 = 类型
    1
    2
    3
    4
    5
    // digit 类型是 number 类型的别名
    type digit = number

    let price: digit
    price = 100
  3. 联合类型:type 别名 = 类型1 | 类型2 | 类型3,表示变量的值可以是若干类型之一
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    type Status = number | string
    type State = "Success" | "Error" | "Unkown"

    function printHttpStatus(status: Status) {
    let state: State;
    if (status.toString().startsWith('2')) {
    console.info(status);
    state = 'Success';
    }
    else if (status.toString().startsWith('4')) {
    console.error(status);
    state = "Error";
    }
    else {
    console.log("unknown status");
    state = "Unkown"
    }
    return state;
    }

    const state404 = printHttpStatus("404");
    const state200 = printHttpStatus("200");
    console.log(`404 is ${state404}, 200 is ${state200}`)
  4. 交叉类型:type 别名 = 类型1 & 类型2 & 类型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
    26
    type Address = {
    country: string;
    province: string;
    city: string;
    district: string;
    code: string
    }

    type BasicInfo = {
    name: string;
    age: number;
    school: string;
    }

    type Person = Address & BasicInfo;

    const lee: Person = {
    name: "lee",
    age: 22,
    school: "NU",
    country: "CN",
    province: "SX",
    city: "XX",
    district: "JJ",
    code: "000000"
    }

type + void 存在的问题

  1. 问题描述:当使用类型声明限制函数返回值为 void 时,TypeScript 不会严格要求函数返回空(即允许函数返回 undefined 之外的值)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type NoReturnFunc = (...args: String[]) => void;

    const printf: NoReturnFunc = (msg) => {
    console.log(msg);
    return 0;
    }

    const funcCode = printf("Hello World");
    console.log(funcCode); // 0

    if (funcCode == 0) // 警告:This comparison appears to be unintentional because the types 'void' and 'number' have no overlap.
    console.log("success!")
  2. 原因解释:TS 这样设计是为了确保像如下的代码成立,避免类型限制影响箭头函数的简写语法。
    1
    2
    3
    4
    5
    6
    7
    8
    const src = [1, 2, 3];
    const dst = [0];

    src.forEach(el => dst.push(el));
    // forEach 的回调函数返回值类型是 void,表示不关心返回值。
    // 这里使用的箭头函数虽然返回了 dst.push(el) 的结果(即 dst 的最新长度),但 TypeScript 将其处理为 void 类型。
    // 这是为了避免因为不必要的返回值引发错误。
    // 同时,void 类型的函数返回值不能被用于比较或赋值,否则会报错。

TS - 类

定义 | 重写

TS 中的类与 JS 相比,有以下注意事项,

  • 属性必须预先声明
  • 重写父类方法时推荐使用 override 关键字,可以增加语法提示,避免出错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
speak() {
console.log(`I'm ${this.name}, and ${this.age} years old now`);
}
}

class Student extends Person {
grade: string
constructor(name: string, age: number, grade: string) {
super(name, age);
this.grade = grade;
}
override speak() {
console.log(`I'm ${this.name}, and ${this.age} years old now, and in Grade ${this.grade}`);
}
}

属性修饰符

image-20241202105548018

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
34
35
36
37
38
39
40
41
42
43
44
class Person {
public name: string
public age: number
protected studentId: string
private readonly identityId: string

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

getStudentId(): string {
return this.studentId;
}

getIdentityId(): string {
return this.identityId
}
}

class Student extends Person {
public printInfo() {
console.log(`name=${this.name}`);
console.log(`age=${this.age}`);
console.log(`studentId=${this.studentId}`);
// console.log(`identityId=${this.identityId}`); // 警告
console.log(`identityId=${this.getIdentityId()}`);
}
}

const stu = new Student("Jack", 19, "132434", "24465767");
console.log(`name=${stu.name}`);
console.log(`age=${stu.age}`);
// console.log(`studentId=${stu.studentId}`);
console.log(`studentId=${stu.getStudentId()}`);
// console.log(`studentId=${stu.identityId}`);
console.log(`studentId=${stu.getIdentityId()}`);

属性的简写

语法:constructor(修饰符 属性名: 类型, 修饰符 属性名: 类型, ...)

属性简写必须包含修饰符,但是当属性是继承于父类/抽象类时,不能包含修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person1 {
/* 完整写法 */
public name: string
public age: number

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

class Person2 {
/* 简写形式(推荐!) */
constructor(public name: string, public age: number) { }
}

TS - 抽象类

  1. 解释:抽象类是一种无法被实例化的类,专门用于定义类的结构和行为,类中可以写抽象方法,也可以写具体实现
  2. 功能:抽象类主要用于为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法
  3. 语法:使用 abstract 关键字修饰类,则为抽象类;修饰方法,则为抽象方法
  4. 适用场景:①定义通用接口 ②提供基础实现 ③确保关键实现 ④共享代码和逻辑
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 包裹 - 抽象类
abstract class Package {
// 构造方法 - weight: 包裹重量
constructor(public weight: number) { }
// 抽象方法 - calculate: 计算包裹运费
abstract calculate(): number
// 具体实现 - printPackage: 打印包裹重量和运费
printPackage() {
console.log(`package weight=${this.weight}, package express fee=${this.calculate()}¥`);
}
}

// 标准包裹
class StandardPackage extends Package {
constructor(weight: number, public unitPrice: number) {
super(weight);
}

calculate(): number {
return this.weight * this.unitPrice;
}
}

// 特快包裹
class ExpressPackage extends Package {
constructor(
weight: number,
public unitPrice: number,
public additional: number
) {
super(weight);
}

calculate(): number {
if (this.weight < 10) {
return this.weight * this.unitPrice;
} else {
return 10 * this.unitPrice + (this.weight - 10) * this.additional;
}
}
}

const s = new StandardPackage(100, 10);
const e = new ExpressPackage(100, 12, 20);

s.printPackage();
e.printPackage();

TS - 接口

  1. 解释:接口(interface)是一种定义结构的方式,用于为类、对象、函数等规定一种契约,从而确保代码的一致性和类型安全。但是注意:interface 只能定义结构,不能包含任何实现
  2. 命名规范:如 IPersonPersonInterface
  3. 适用场景:①定义对象的格式 ②类的契约 ③自动合并

定义 | 使用

类结构

类使用 implements 关键字实现已有的接口,规范类的实现。

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

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

sayHello() { console.log("Hello World!"); }
}

const p = new Person("Jack", 19);
p.sayHello();

对象结构

接口可以看作一个类型,可以使用 :接口名 的方式,限制对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IUser {
name: string
age?: number // 可选属性
readonly gender: string // 只读属性
sayHello(): void
}

const user: IUser = {
name: "Jack",
gender: "Male",
age: 19,
sayHello() { console.log("Hello World!") }
}

函数结构

接口可以看作一个类型,也可以使用 :接口名 的方式,限制函数的类型。

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

const add: IAdd = (x, y) => x + y

console.log(add(1, 2))

继承

接口可以通过 extends 关键字实现继承。

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

interface IStudent extends IPerson {
grade: string
}

const stu: IStudent = {
name: "Jack",
age: 19,
grade: "M3"
}

自动合并

同名接口的重复定义并不会导致接口覆盖,而是自动合并

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

interface IPerson {
grade: string
}

const stu: IPerson = {
name: "Jack",
age: 19,
grade: "M3"
}

与 type 和抽象类的区别

image-20241202112854852

TS - 泛型

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

  2. 使用

    • 泛型函数

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

      log<number>(100);
      log<string>('Hello World');
    • 多个泛型

      1
      2
      3
      4
      5
      6
      7
      function log<T, U>(msg1: T, msg2: U): T | U {
      console.log(msg1, msg2)
      return Date.now() % 2 ? msg1 : msg2
      }

      log<number, string>(100, 'big number');
      log<string, boolean>('happy now', false);
    • 泛型接口

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

      let p: IPerson<string> = {
      name: "Jack",
      age: 19,
      extraInfo: "Male"
      }
    • 泛型约束

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      interface IPerson {
      name: string
      age: number
      }

      function logPerson<T extends IPerson>(info: T): void {
      console.log(`name=${info.name}, age=${info.age}`);
      }

      logPerson({ name: "Jack", age: 19 })
    • 泛型类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class Person<T> {
      constructor(
      public name: string,
      public age: number,
      public extraInfo: T
      ) { }
      }

      interface IJobInfo {
      title: string,
      company: string
      }

      const p = new Person<IJobInfo>("Jack", 19, { title: "web", company: "ccd" })

TS - 类型声明文件

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

  2. 语法:使用 declare 关键字,为现有代码提供类型信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // demo.js
    export function add(a, b) {
    return a + b;
    }

    export function mul(a, b) {
    return a * b;
    }

    // demo.d.ts,为现有的同名 .js 文件中的代码添加类型信息
    declare function add(a: number, b: number): number;
    declare function mul(a: number, b: number): number;
    export {add, mul};

    // index.ts
    import { add, mul } from "./demo.js"

    const x = add(2, 4); // 会有类型提示
    const y = mul(4,5); // 会有类型提示

    console.log(x, y);

TS - 装饰器

概述

  • 装饰器本质是一种特殊的函数,它可以对类、属性、方法、参数进行扩展,同时能让代码更简洁。

  • 装饰器自 2015 年在 ECMAScript-6 中被提出到现在,已将近 10 年。

  • 截止目前,装饰器依然是实验性特性,需要开发者手动调整配置来开启装饰器支持。

  • 装饰器有五种:类装饰器属性装饰器方法装饰器访问器装饰器参数装饰器

  • 虽然 TypeScript 5.0 中可以直接使用类装饰,但为了确保其他装饰器可用,现阶段使用时,仍建议使用experimentalDecorators 配置来开启装饰器支持

    img

类装饰器

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

  2. 语法:类装饰器的定义使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* 类装饰器 - 定义 :一个可接受目标类为参数的函数 */
    /* 注意-1:target 参数就是要被装饰的类 */
    /* 注意-2:类装饰器可以返回一个新的类,替换掉被装饰的类;否则,被装饰的类不会被替换 */
    function ClassDecoratorDemo(target: Function) {
    console.log(`类装饰器被调用,收到的参数为 ${target}`);
    }

    /* 类装饰器 - 使用:通过 @装饰器名 的方式将类装饰器应用于类 */
    /* 注意:类装饰器的调用时机为类定义阶段,类实例化之前 */
    @ClassDecoratorDemo // @ClassDecoratorDemo <==> ClassDecoratorDemo(Person)
    class Person {
    constructor(public name: string, public age: number) {
    console.log(`类构造函数被调用`);
    }
    }
  3. 构造类型声明:在 TypeScript 中,Function 类型可以表示诸如普通函数、箭头函数、方法等等,但是 Function 类型的函数并非都可以使用 new 关键字实例化(如箭头函数),因此我们需要定义一个新的类型用于限制类装饰器中的 target 参数类型,该类型称之为构造类型,共有以下两种方式声明构造类型。

    • 声明构造类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /* 仅声明构造类型 */
      /*
      - new 表示 该类型可以使用 new 关键字进行实例化,即该类型是构造类型
      - ...args 表示 该类型可以接受任意数量的参数
      - any[] 表示 该类型可以接受任意类型的参数
      - {} 表示 该类型的返回类型是对象(非 null、非 undefined)
      */
      type Constructor = new (...args: any[]) => {};
      function ClassDecoratorDemo(target: Constructor) {}

      @ClassDecoratorDemo
      class Person {}
    • 声明构造类型 + 指定静态属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* 声明构造类型 + 指定静态属性 */
      type Constructor = {
      new (...args: any[]): {}; // 构造类型签名
      layer: string; // 静态属性 layer(即指定的构造类型必须要有 layer 静态属性)
      };
      function ClassDecoratorDemo(target: Constructor) {}

      @ClassDecoratorDemo
      class Person {
      static layer = "elite";
      }
  4. 应用举例-1:定义一个装饰器,实现 Person 实例调用 toString 时返回 JSON.stringify 的执行结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* 自定义构造类型 */
    type Constructor = new (...args: any[]) => {};

    /* 自定义类装饰器,用于新增属性和方法 */
    function ToStringDecorator(target: Constructor) {
    // 修改 Person 的 toString 方法
    target.prototype.toString = function () {
    return JSON.stringify(this); // this 指向调用 toString 方法的 Person 实例
    };
    // “密封” Person 的原型对象,此时 ①阻止添加新属性 ②阻止删除现有属性 ③保持现有属性的可配置性
    Object.seal(target.prototype);
    }

    /* 使用类装饰器 */
    @ToStringDecorator
    class Person {
    constructor(public name: string, public age: number) {}
    }

    const tom = new Person("Tom", 18);
    console.log(tom.toString());
  5. 应用举例-2:设计一个 LogTime 装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法,用于读取创建时间。

    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
    34
    35
    /* 自定义构造类型 */
    type Constructor = new (...args: any[]) => {};

    /* User 类型约束 */
    interface User {
    createdAt: Date;
    getCreatedAt: () => string; // 等价于 getCreatedAt(): string
    }

    /* 自定义类装饰器,用于新增属性和方法 */
    function LogTime<T extends Constructor>(target: T) {
    return class extends target {
    public createdAt: Date; // 实例创建时间

    constructor(...args: any[]) {
    super(...args); // 调用父类的构造函数
    this.createdAt = new Date();
    }

    getCreatedAt() {
    return `该对象创建于 ${this.createdAt}`;
    }
    };
    }

    /* 使用类装饰器 */
    @LogTime
    class User {
    constructor(public name: string, public age: number) {}
    }

    const tom = new User("Tom", 18);
    console.log(JSON.stringify(tom));
    console.log(tom.getCreatedAt());
    console.log(tom.createdAt);

装饰器工厂

  1. 解释:装饰器工厂是一个返回装饰器的函数,可以为装饰器添加参数,从而更灵活地控制装饰器的行为。

  2. 应用举例:定义一个 LogInfo 类装饰器工厂,实现 Person 实例可以调用 introduce 方法,且 introduce 调用内容输出的次数由 LogInfo 接收的参数决定。

    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
    /* 构造类型 */
    type Constructor = new (...args: any[]) => {};

    /* Person 类型约束 */
    interface Person {
    introduce: () => void; // 等价于 introduce(): void
    }

    /* 类装饰器工厂 */
    function DecoratorFactory(repetition: number) {
    return function (target: Constructor) {
    // 向实例的原型上添加方法 introduce
    target.prototype.introduce = function () {
    for (let i = 0; i < repetition; i++) {
    console.log(`My name is ${this.name}, and I'm ${this.age} years old.`);
    }
    };
    };
    }

    /* 使用类装饰器 */
    @DecoratorFactory(5)
    class Person {
    constructor(public name: string, public age: number) {}
    }

    const tom = new Person("tom", 20);
    tom.introduce();

装饰器组合

解释:一个类可以组合使用多个装饰器装饰器工厂,其执行顺序为 ① 由上到下执行所有的装饰器工厂,依次获取到对应的装饰器 ② 由下到上执行所有的装饰器

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
34
/* 自定义构造类型 */
type Constructor = new (...args: any[]) => void;

/* 装饰器 */
function test1(target: Constructor) {
console.log("test1");
}

/* 装饰器工厂 */
function test2() {
console.log("test2工厂");
return function (target: Constructor) {
console.log("test2");
};
}

/* 装饰器工厂 */
function test3() {
console.log("test3工厂");
return function (target: Constructor) {
console.log("test3");
};
}

/* 装饰器 */
function test4(target: Constructor) {
console.log("test4");
}

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

上述代码执行输出结果为

1
2
3
4
5
6
test2工厂
test3工厂
test4
test3
test2
test1

属性装饰器

  1. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 属性装饰器
    * @param target 对于静态属性,该参数为类;对于实例属性,该参数为类的原型对象
    * @param propertyKey 属性名
    */
    function FieldDecoratorDemo(target: object, propertyKey: string) {
    console.log(target, propertyKey);
    }

    class Person {
    /* 属性装饰器的使用 */
    @FieldDecoratorDemo public name: string; // 等价于 FieldDecoratorDemo(prototype, name)
    @FieldDecoratorDemo public age: number; // 等价于 FieldDecoratorDemo(prototype, age)
    @FieldDecoratorDemo public static layer = "elite"; // 等价于 FieldDecoratorDemo(Person, layer)
    constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    }
    }
  2. 属性遮蔽:以下代码,如果片段 (2) 在片段 (3) 上边,那么实例化 Person 时,this.name = name 创建实例属性 name,而 this.age = age 则是调用原型对象上的 setter,用于修改全局变量 __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
    // (1)
    class Person {
    public name: string;
    public age: number;
    constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    }
    }

    // (2)
    let __age = 130;
    Object.defineProperty(Person.prototype, "age", {
    get() {
    return __age;
    },
    set(newAge) {
    __age = newAge;
    },
    });

    // (3)
    const p = new Person("Tom", 19);
    console.log(p.age);
  3. 应用举例:定义一个 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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    /**
    * 用于监听实例属性修改的属性装饰器
    * @param target 原型对象(类的 prototype)
    * @param propertyKey 被装饰的属性名
    */
    function State(target: object, propertyKey: string) {
    // 定义一个私有属性名,用于存储实际的值
    const key = `__${propertyKey}`;

    // 在原型对象上定义属性的 getter 和 setter
    Object.defineProperty(target, propertyKey, {
    get() {
    // getter 用于获取存储在实例上的私有属性值
    return this[key]; // `this` 指向实例对象
    },
    set(v) {
    // setter 用于监控属性值的修改并更新存储的值
    console.log(`实例属性 ${propertyKey} 被修改了: ${this[key]} --> ${v}`);
    this[key] = v; // 更新存储的私有属性值
    },
    enumerable: true, // 属性可枚举
    configurable: true, // 属性描述符可配置
    });
    }

    class Person {
    public name: string; // 定义实例属性 name
    @State age: number; // 为 age 属性添加装饰器,使其成为响应式属性
    constructor(name: string, age: number) {
    this.name = name; // 初始化实例属性 name
    this.age = age; // 通过 setter 初始化 age 属性,本质上是设置私有属性 __age 的值
    }
    }

    // 创建实例并验证功能
    const p1 = new Person("Tom", 10); // 输出: 实例属性 age 被修改了: undefined --> 10
    const p2 = new Person("Jack", 19); // 输出: 实例属性 age 被修改了: undefined --> 19
    console.log(p1, p2); // 输出: Person { name: 'Tom', __age: 10 } Person { name: 'Jack', __age: 19 }

    /* ------------------------------------------------------------------------------- */
    /* 上述代码可以完全等价为下述代码 */
    class Animal {
    public name: string;
    public age: number;

    /* 响应式实现-1,等同上述的属性装饰器 */
    private __age: number = 0;

    constructor(name: string, age: number) {
    /* 响应式实现-2,监视 age 属性的修改,其中 age 属性存放在类的原型对象上,被代理的数据存储在实例对象上 */
    Object.defineProperty(Animal.prototype, "age", {
    get() {
    return this.__age;
    },
    set(v) {
    console.log(`实例属性 age 被修改了:${this.__age} --> ${v}`);
    this.__age = v;
    },
    enumerable: true,
    configurable: true,
    });

    this.name = name;
    this.age = age;
    }
    }

    const a1 = new Animal("Happy", 20);
    const a2 = new Animal("Lucky", 12);
    console.log(a1, a2);
    补充 Object.defineProperty 的使用
    1. 功能:Object.defineProperty() 在给定的对象上定义一个新属性或修改一个现有属性,同时返回修改后的对象。

    2. 语法:Object.defineProperty(obj, prop, descriptor)

      • obj:给定的对象
      • prop:要定义或修改的属性名,可以是 string 或 Symbol
      • descriptor属性描述符,用于控制要定义或修改的属性的行为
    3. 返回值:定义或修改了指定属性后的给定对象。

    4. 使用说明

      • 属性描述符(property descriptor)可以分为数据描述符(data descriptor)和访问器描述符(accessor descriptor),对应的属性可以称之为数据属性访问器属性
        • 属性描述符是一个对象,其中数据描述符所特有的配置项为 valuewritable,访问器描述符所特有的配置项为 getset,二者所共有的配置项为 enumerableconfigurable
        • 一个属性描述符只能是数据描述符或访问器描述符二者其一
        • 如果一个属性描述符不包含 valuewritablegetset 属性,则该属性描述符被看作数据描述符。
        • 如果一个属性描述符同时包含 【valuewritable】和【getset】,则会报错。
      • 默认情况下,Object.defineProperty() 定义的属性是不可写(not writable)、不可枚举(not enumerable)、不可配置(not configurable)的。而通过 object.propertyName=propertyValue 这样的方式定义的属性是可写可枚举可配置的。
      • Object.defineProperty() 本质上是通过 JavaScript 的内在方法(internal method)[[DefineOwnProperty]] 实现的,与另一个 JavaScript 的内在方法 [[Set]] 无关,因此,通过该方法修改一个现有属性不会导致该属性的 setter 被调用。
    5. descriptor 配置(都是可选的!)

      • 共有的配置项(shared keys)

        • configurable:属性的类型是否可以切换(数据属性 ⇔ 访问器属性);属性是否可以被删除;该属性的属性描述符的其他配置项是否可以被修改。默认值为 false

          特殊情况:对于数据描述符而言,如果配置了 writable: true,那么无论如何,该属性的值都可以被修改,同时 writable 配置项可以被修改为 false

        • enumerable:属性是否可枚举(如 for...inObject.keys)。默认值为 false

      • 数据描述符的配置项

        • value:属性对应的值,可以是任意合法的 JavaScript 值。默认值为 undefined
        • writable:属性是否可以通过赋值操作符(assignment operator)被修改。默认值为 false
      • 访问器描述符的配置项

        • get:属性的 getter,访问该属性时调用,不接受参数,同时将其返回值视作属性值。默认值为 undefined
        • set:属性的 setter,对该属性赋值时调用,接受一个参数(即赋的新值)。默认值为 undefined

        注意事项:getset 方法被调用时,函数中的 this 指向的是访问或修改该属性的对象。需要注意的是,由于 JavaScript 的继承机制,this 指向的不一定是属性所定义的对象。

方法装饰器

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

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

    @MethodDecoratorDemo speak() {
    // 等价于 MethodDecoratorDemo(prototype, speak, descriptor)
    console.log(`Hi, my name is ${this.name}, and my age is ${this.age}`);
    }

    @MethodDecoratorDemo static isAdult(age: number) {
    // 等价于 MethodDecoratorDemo(Person, isAdult, descriptor)
    return age >= 18;
    }
    }
  2. 应用举例:定义一个 Logger 方法装饰器,用于在方法执行前和执行后,追加一些额外的逻辑;一个 Validate 方法装饰器,用于验证数据。

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    /**
    * 用于在实例方法执行前后追加一些额外逻辑的方法装饰器
    * @param target 类的原型对象
    * @param propertyKey 方法名
    * @param descriptor 方法描述对象
    */
    function Logger(
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {
    /* 1. 缓存原始方法 */
    const original = descriptor.value;
    /* 2. 替换原始方法 */
    descriptor.value = function (...args: any[]) {
    console.log(`正在准备执行${propertyKey}方法······`);
    const result = original.call(this, ...args);
    console.log(`${propertyKey}方法已执行完毕!`);
    return result;
    };
    }

    /**
    * 用于验证静态方法参数合法性的方法装饰器工厂
    * @param maxValue 最大参数值
    * @returns 方法装饰器
    */
    function Validate(maxValue: number) {
    return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {
    /* 1. 缓存原始方法 */
    const original = descriptor.value;
    /* 2. 替换原始方法 */
    descriptor.value = function (...args: any[]) {
    if (args[0] > maxValue)
    throw new Error(
    `非法的参数,所传入参数值 ${args[0]} 大于最大允许传入值为 ${maxValue}`
    );
    return original.call(this, ...args);
    };
    };
    }

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

    @Logger
    speak(repetition: number) {
    for (let i = 0; i < repetition; i++) {
    console.log(`My name is ${this.name}, and my age is ${this.age}`);
    }
    }

    @Validate(150)
    static isAdult(age: number) {
    return age >= 18;
    }
    }

    const p = new Person("Tom", 23);
    p.speak(5);
    console.log(Person.isAdult(149));
    console.log(Person.isAdult(200));
    补充 Function.prototype.apply/call/bind 的使用
    1. Function.prototype.call():以给定的 this 值和逐个提供的参数调用该函数。
      • 语法:call(thisArg[, arg1, arg2, /* …, */ argN])
      • 参数
        • thisArg:调用 func 时要使用的 this 值。非严格模式下,nullundefined 被替换为全局对象,原始值被转换为对象。
        • arg1, arg2, /* …, */ argN:可选的若干函数参数。
      • 返回值:使用指定的 this 值和若干可选参数调用函数后的结果。
    2. Function.prototype.apply():以给定的 this 值和以数组(或类数组对象)提供的参数调用该函数。
      • 语法:apply(thisArg, argsArray)
      • 参数
        • thisArg:调用 func 时要使用的 this 值。非严格模式下,nullundefined 被替换为全局对象,原始值被转换为对象。
        • argsArray:可选的类数组对象,用于指定调用 func 时的参数。
      • 返回值:使用指定的 this 值和可选的类数组对象提供的参数调用函数后的结果。
    3. Function.prototype.bind():根据原始函数创建一个新函数,同时指定该新函数的 this 值和初始参数。
      • 语法:bind(thisArg, arg1, arg2, /* …, */ argN)
      • 参数
        • thisArg:调用新函数时要使用的 this 值。非严格模式下,nullundefined 被替换为全局对象,原始值被转换为对象。如果新函数被当作构造函数使用,则该参数会被忽略。
        • arg1, arg2, /* …, */ argN:调用新函数时,插入到传入新函数中的参数前的可选的初始参数。
      • 返回值:使用指定的 this 值和可选的初始参数创建的新函数。

访问器装饰器

  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
    /**
    * 访问器装饰器
    * @param target 对于静态访问器,该参数是类;对于实例访问器,该参数是类的原型对象
    * @param propertyKey 访问器名称
    * @param descriptor 访问器的描述对象,对应 get 和 set 属性是被装饰的访问器方法
    */
    function AccessorDecoratorDemo(
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {
    console.log(target, propertyKey, descriptor);
    }

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

    @AccessorDecoratorDemo get school() {
    return "XDU";
    }

    @AccessorDecoratorDemo static get address() {
    return "Xi'an";
    }
    }
  2. 应用举例:对 Weather 类的 temp 属性的 set 访问器进行限制,允许设置的取值范围为 [-50, 50]

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /**
    * 生成限制数值范围的访问器装饰器工厂
    * @param min 最低取值
    * @param max 最高取值
    * @returns 限制数值范围的访问器装饰器
    */
    function RangeValidate(min: number, max: number) {
    return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {
    /* 1. 缓存原始的 setter */
    const original = descriptor.set;
    /* 2. 替换 setter,加入范围验证逻辑 */
    descriptor.set = function (value: number) {
    if (value < min || value > max)
    throw new Error(
    `${propertyKey} 的取值范围应该在 ${min}${max} 之间`
    );
    if (original) original.call(this, value);
    };
    };
    }

    class Weather {
    private _temp: number;

    constructor(temp: number) {
    this._temp = temp;
    }

    @RangeValidate(-50, 50) set temp(value: number) {
    this._temp = value;
    }

    get temp() {
    return this._temp;
    }
    }

    const w = new Weather(20);

参数装饰器

  1. 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 参数装饰器
    * @param target 修饰静态方法的参数时,该参数是类;修饰实例方法的参数时,该参数是类的原型对象
    * @param propertyKey 参数所在方法的名称
    * @param parameterIndex 参所在方法的参数列表中的索引(0-based)
    */
    function ArgumentDecoratorDemo(
    target: object,
    propertyKey: string,
    parameterIndex: number
    ) {
    console.log(target, propertyKey, parameterIndex);
    }

    class Person {
    constructor(public name: string) {}

    speak(@ArgumentDecoratorDemo message: string) {
    console.log(`${this.name} say: ${message}`);
    }
    }
  2. 应用举例:定义方法装饰器 Validate,同时搭配参数装饰器 NotNumber,来对指定方法的参数类型进行限制。NotNumber 限制指定参数不能是数值类型,Validate 来验证使用 NotNumber 装饰器的参数是否满足条件。

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    /**
    * 记录类型不为 number 的参数的参数装饰器
    * @param target 类的原型对象
    * @param propertyKey 参数所在的方法名
    * @param parameterIndex 参数在方法参数列表中的索引
    */
    function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
    /* 存储方法 propertyKey 中所有限制类型不为数字的参数索引 */
    const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
    notNumberArr.push(parameterIndex);
    target[`__notNumber_${propertyKey}`] = notNumberArr;
    }

    /**
    * 验证是否所有使用了 NotNumber 参数装饰器的参数都符合条件
    * @param target 类的原型对象
    * @param propertyKey 方法名
    * @param descriptor 方法的描述对象
    */
    function Validate(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {
    /* 缓存原始方法 */
    const originalMethod = descriptor.value;
    /* 替换原始方法 */
    descriptor.value = function (...args: any[]) {
    const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
    notNumberArr.forEach((index) => {
    if (typeof args[index] === "number")
    throw new Error(`第 ${index} 个参数不能为 number`);
    });
    return originalMethod.call(this, ...args);
    };
    }

    class Person {
    constructor(public name: string) {}

    @Validate
    speak(@NotNumber message: any) {
    console.log(`${this.name} say: ${message}`);
    }
    }

    const p = new Person("Tom");
    p.speak("Hello");
    p.speak(1);
本贴参考