前端笔记之typescript基础

前端笔记之typescript基础

  • 1. 数据类型:
  • 2. 联合类型
  • 3. 接口
  • 4. 函数
  • 5. 类型别名
  • 6. 元组
  • 7. 枚举
  • 8. 类
    • 8.1 构造器
    • 8.2 属性权限描述符
    • 8.3 存取器
    • 8.4 类的继承
    • 8.5 静态方法和静态属性
    • 8.6 readonly修饰符
    • 8.7 抽象类
    • 8.8 接口继承类
  • 9. 声明合并
  • 10. 泛型
    • 10.1 泛型定义
    • 10.2 泛型约束
    • 10.3 泛型接口
  • 11. 声明文件和内置对象
    • 11.1 声明文件
    • 11.2 内置对象
  • 12. typescript5.0之后的装饰器使用
    • 12.1 类装饰器
    • 12.2 类方法装饰器
    • 12.3 类属性装饰器
    • 12.4 自动访问器
    • 12.5 getter、setter装饰器

其实我是不太希望把typescript放在前端的,毕竟在node的工具下,js也慢慢脱离了前端脚本的桎梏,作为规范化超集的typescript其实在写服务器服务的场景下也能有不错的表现。

1. 数据类型:

基本数据类型:boolean、number、string、undefined、null、symbol、bigint。
引用数据类型:array、object、tuple、enum;
特殊数据类型:any、void、never、unknown。(typescript并没有特殊声明这些为特殊类型)

这里注重讲解never和unknown。

never标识永远不存在的数据类型,例如一个函数在运行中被抛出错误,中断执行了,那么因为没有运行完毕,所以不会获取对应的返回值或者返回void,所以存在一个不会存在的返回值,那么这个函数的返回值就会是never。never的值可以赋值给所有的其他类型,但是never类型只能接收never类型的值。
unknown类似于any,但是比any更为安全,它是用来表示当前不知道的数据类型,所有的数据类型都能给unknown赋值,但是unknown类型的只能给any和unknown赋值。那么对一个unknown类型的值再具体使用它之前就要进行类型收缩或者断言才能确定出他的数据类型来进一步的使用。

2. 联合类型

表示可以取值为多种类型中的一种。
当一个变量可以为多种数据类型中不定的一种的时候,我们可以使用any或者unknown来承接,但是这两种有个问题,那就是来者不拒。如果这一种类型我们明确知道是少数的几种类型,比如1和0和true和false在某种情况上是等价的,特别是在后端某些语言中强转类型的时候是互为number和Boolean的通用类型。那么就会出现前端接收的一个参数极可能是数字也可能是boolean,那么定义方式可以使用联合类型声明:

const flag: number|boolean = 1; // 或者true

3. 接口

在面向对象中,接口是对对象类行为的抽象描述,这一点在java等语言中极为明显,JavaScript作为一门脚本性语言一直被广大面向对象的编程语言排斥在外,除了它不能打破浏览器沙盒以外,就是它在es6之前没有显示的面向对象的语法概念,对象通过function来创建,而非大众化的class,更遑论接口了。typescript引入接口概念后标志着JavaScript正式成为了一门更显而易见的面向对象的编程语言。
typescript主要对JavaScript修正其实就是编译阶段强类型规范,所以typescript中接口比java等语言抽象作用上更注重于描述的作用,即是介绍继承这个接口的对象内部属性是什么样子的。举个例子:

例如有一个对象,内部有两个属性,name和age,因为typescript的强类型规范,所以在定义的时候我们就要声明它内部的属性,方便我们来调用,如果这时候我们对这个对象之后的功能具有不确定性,无法直接去声明这么一个对象,我们可以使用一个接口去描述这个对象。

    interface Person{
      name: string, //逗号和分号都可以
      age: number
    }
    // 可以直接通过接口描述去定一个对象
    const sam: Person = {name: "sam", age: 30};

当然接口更为重要的是它的抽象性,以便于更为具体的 对象类 可以继承,已达到规范和扩展的作用,新的具体类可以使用 implements 来继承。继承具有完全性,即接口所声明的属性和方法都必须为类所继承。

  interface Person{
      age: number,
      name: string,
      walk: () => void
    }
    // 必须全部继承,但是可以多出自己的属性,举个例子,爸爸的钱是儿子的,儿子除了继承爸爸的财产还能自己挣更多的钱
    class man implements Person{
      age: number;
      name: string;
      sex: string;
    
      constructor(age, name, sex) {
        this.age = age;
        this.name = name;
        this.sex = sex;
      }
      walk(): void {
        console.log("walk")
      }
    }

任意属性,当定义接口时,不清楚当前属性名称以及属性类型可以使用任意属性的写法。

    interface Person{
      age: number,
      name: string,
      [propName: string]: any,// 一旦确定为具体类型,那么所有已经定义的属性的类型必须为这个具体类型的子集
      walk: () => void
    }

可选类型,当定义接口时,一个属性时可有可无的可使用 ? 进行声明。

 interface Person{
      age: number,
      name: string,
      [propName: string]: any,// 一旦确定为具体类型,那么所有已经定义的属性的类型必须为这个具体类型的子集
      walk?: () => void
    }

众所周知,JavaScript中数组是一个伪对象。那么对象类可以为接口所约束,数组同样可以。数组这个object的可以是数字的连续的整数,那么就知道了属性名类型为number,就可以写接口了。例如有这么一个数组全部是数字类型,可以让它继承这么一个接口:

 interface MyArray{
      [index: number]: number
    }

不过很鸡肋罢了,基本都在使用泛型来约束。

其实接口同样可以约束函数,因为所有的函数都被Function这个接口所约束着。所以任何的函数同样可以被接口所约束,无论这个接口有没有显式的继承Function。

// 可以不用继承Function,反正所有的函数都是继承于Function的
 interface MyFunction extends Function{
   (a:number, b:string): boolean;
 }
 // 如此定义函数的时候当参数名和类型以及返回值的类型不和接口相同的时候,就会报错。
 const fun:MyFunction = (a: number, b:string):boolean => {
   return true;
 }

4. 函数

函数声明,和JavaScript一样,不同的就是强调了参数和返回值的类型

//使用冒号来声明参数和返回值的类型
 function fun(param1: string, param2: number): boolean {
   return true;
 }

可选参数,对于一些参数是可有可无的,可以使用 ? 来声明参数,但是有个约束条件就是,可选参数必须在所有默认参数的后面。

//使用冒号来声明参数和返回值的类型
function fun(param1: string, param2: number, param3?:number): boolean {
   return true;
 }
 const fun2 = (a: number, b:string):boolean => {
   return true;
 }

上面已经接受了接口约束函数,既然接口可以被约束,这就说明在 匿名函数 的情况下,约束条件是存在于表达式两侧的,所以完整的声明函数表达式如下:

const fun: (a:number, b: string) => boolean =  (a: number, b:string):boolean => {
  return true;
};

剩余参数,也就是不知道会被传输多少参数的情况下可以使用rest获取剩余参数

const fun: (a:number, b: string, ...rest: number[]) => boolean =  (a: number, b:string):boolean => {
   	return true;
};

函数重载,在JavaScript中是不会允许同名函数一起存在的,那么只会是最后定义的函数会被保留,其余的都会被覆盖,即便是同名但是功能不一样的函数,在java等语言中是允许这个同名函数的存在的,那么区别就是函数参数的不同。例如有一个一些复杂的多参函数,比如第一个参数是数字,那么第二个参数也得是数字;第一个参数是字符串第二个参数也得是字符串。我们一般会这么写:

function setData(value1: string|number, value2: string|number){
    if(typeof value1 === 'string' && typeof value2 === 'string'){
        // 操作
    }else if(typeof value1 === "number" && typeof value2 === 'number'){
        // 操作
    }
}

这个写法是没有错的,但是在typescript文档注释行程中,参数是一个联合类型的样子。
在这里插入图片描述
但是这样不能准确的描述出一个参数的需求文档解释,会让人以为在一个参数确定之后,第二个参数依旧可以在两个类型中二选一,于是要使用函数重载。

function setData(value1: string, value2:string):void;
function setData(value1: number, value2:number):void;
function setData(value1: string|number, value2: string|number){
  if(typeof value1 === 'string' && typeof value2 === 'string'){
    // 操作
  }else if(typeof value1 === "number" && typeof value2 === 'number'){
    // 操作
  }
}

于是我们可以得到准确的文档提示。
前端笔记之typescript基础_第1张图片前端笔记之typescript基础_第2张图片

5. 类型别名

type关键词可以让用户自己定义类型别名或者声明类型,比如你声明了一个名字很长的class或者接口,你在使用的使用,可以声明他的简短别名来使用;或者你临时在单独一个文件中使用一个属性很多但是不需要class或者interface的类型时也可以使用type声明;还有就是你是用的变量允许三五个类型的使用联合声明显得代码冗长的时候也可以使用type;当你想要对你的变量进行赋值约束也可是使用type,总之,type主打的就是一个代码优化。

6. 元组

元组(tuple)不同于Python的元组,typescript中的元组其实就是对数组的一个优化,比如一个数组是能存放不显朗、不同类型比较随意的数据集合。但是元组不同,它是被固定好了数据类型及其顺序以及个数的数组。他并没有专门的关键字,主要是以数组的形式来声明。

 const tuple: [number, string] = [1, "2"] //只能允许第一个字符为number、第二个字符是字符串,且初始化的时候长度只能为2

它的内置方法和数组一样,翻译成JavaScript就是一个特殊的数组而已,那么这里面就会有一些数组元素改变的方法,比如push、shift、unshift、reverse等,其实都是允许的,它的约束性只存在于初始化的时候,所以我感觉也是一个比较鸡肋的,目前在我开发中用处不大。

7. 枚举

enmu做的就是为一些特殊情况的所有选项做一个描述,类似于object的键值对,但是有一些不同,比如object不允许非string的key,不允许非键值对形式的定义,而枚举可以,而且枚举是值和键互为索引的,用值能取到键,用键可以取到值。枚举定义如下所示:

  // 在这种的写法下,t默认值为0,也就是Temp.t代表的是0,e是1,以此类推。
    enum Temp{
      t,
      e,
      m,
      p
    }
    // 在这种的写法下,t默认值为所对应的字符值,也就是Temp.t代表的是"te",e是"ee",以此类推。
    enum Temp{
      t = "te",
      e = "ee",
      m = "me",
      p = "pe"
    }

那我们把上面第一种写法的编译后的js拿出来看一下:

var Temp;
(function (Temp) {
    Temp[Temp["t"] = 0] = "t";
    Temp[Temp["e"] = 1] = "e";
    Temp[Temp["m"] = 2] = "m";
    Temp[Temp["p"] = 3] = "p";
})(Temp || (Temp = {}));

好,那我们看一下这个代码,其实不难发现enmu本质还是对象,不过是一个键值对互为键值对的对象我们把上面的Temp打印一下为:

{ '0': 't', '1': 'e', '2': 'm', '3': 'p', t: 0, e: 1, m: 2, p: 3 }

这样enmu就一目了然了。

枚举有常数项计算所得项两种。计算所得项之后不在允许声明未赋值的枚举项。

枚举类型有常数枚举外部枚举

常数枚举指的是使用 const 声明定义的枚举,常数枚举会在编译阶段删除且不允许定义计算所得项,常量枚举的引用也单纯的变成了对常量的引用。

外部枚举适用于声明文件中,用declare enum语法来声明枚举类型。这样,TypeScript编译器就会假设这个枚举已经存在,并且可以在代码中使用它,所以外部枚举只能描述已经存在的枚举,外部枚举的值在编译时已经确定,不能包含运算成员。

8. 类

类是面向对象编程中最常见的用来抽象对象的描述方法,主要用来描述这一类的对象有怎么样的属性和方法。而对象则是对于类的实现,就是“犬科”和真实的一条狗的区别。

学过面向对象编程的都知道其三大特性:封装、继承、多态。

封装简单的讲就是把这一类特性方法隐藏起来,只暴露出对外的接口方法,给外部想让外部知道的信息,然外部调用想让外部调用的操作。

继承就是子类可以在父类的基础上拓展,由于可以直接使用父类的属性和方法,避免了再一次重复的属性方法的定义赋值。

多态就是同一个父类可以用不同子类呈现,从而展现自上而下不断增多的树形联系,各个子类都有其多特特性,又有公共父类作为连接。

而JavaScript由于长期没有显示的向class这样方式导致一直被其他面向对象的语言所排斥,所以class这部分内容无疑是typescript这个超集最为侧重的一部分内容。

8.1 构造器

学过java的都知道,一个类如果没有构造器是无法被实现的。typescript同样采用了这个观念。constructor就是ts保留的构造器关键词。一个类的声明如下:

class Dog{
   name: string;
   age: number;
   constructor(name: string, age: number, sex: string){
       this.name = name;
       this.age = age;
       // 即便是外部没有声明的属性与就可以赋值
       // 严格模式:有种你试试?
       this.sex = sex;
   }
 }
 // 一个类的实现
 const dog = new Dog("大黄", 5, "公狗");

8.2 属性权限描述符

  1. public:公共修饰符,表示成员可以在类内部和外部访问。如果没有指定修饰符,默认为public。
  2. private:私有修饰符,表示成员只能在类内部访问。私有成员对于类的外部是不可见的。
  3. protected:受保护修饰符,表示成员可以在类内部和派生类中访问。受保护成员对于类的外部是不可见的。

主要区别是在private和protected中,下面一段代码可以展示:

class Test{
  private _name: string;
  protected age: number = 18;
  constructor(name) {
    this._name = name;
  }

  private name: string = "ss";
}
class TestE extends Test {
  sex: number;
  constructor(name, age, sex ) {
    super(name);
      // 这里可以访问到父级的age
    console.log(this.age);
    this.sex = sex;
  }
}
const t = new TestE("sx", 15, 1);
console.log(t);

8.3 存取器

类的存取器(accessor)是一种特殊的方法,用于控制对类成员的访问。提取器由get和set关键字定义,分别用于获取和设置类成员的值。

以下是一个使用提取器的示例:

class Person {
  private _name: string;

  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}
let person = new Person();
person.name = 'Alice'; // 调用set方法设置name属性的值
console.log(person.name); // 调用get方法获取name属性的值,输出:'Alice'

在typescript5.0之后推出了accessor,可以更简便写存取器了。

8.4 类的继承

面向对象的三大特性有两个是关于继承的可见继承的重要性了。

下面是类与类的继承的实例:

class Animal {
  	name: string;
   	constructor(name: string) {
     	this.name = name;
   	}
   	move(distance: number = 0) {
     	console.log(`${this.name} moved ${distance}m.`);
   	}
 }
    
class Dog extends Animal {
   	age: number;
   	constructor(name: string, age: number) {
       supper(name);
    this.age = age;
  	}
  	bark() {
    	console.log('Woof! Woof!');
  	}
}
 const dog = new Dog('Buddy');
 dog.move(10); // 输出: Buddy moved 10m.
 dog.bark(); // 输出: Woof! Woof!

typescript是不支持多重继承的,java对此表示很赞,不像c plus一样,一个类都不知道有几个野爹。

类与接口的继承,接收接口的时候已经说过了,这里在增加一个例子。

interface Shape {
   color: string;
   area(): number;
}
    
class Circle implements Shape {
   color: string;
   radius: number;
   constructor(color: string, radius: number) {
     this.color = color;
     this.radius = radius;
   }
   area() {
     return Math.PI * this.radius * this.radius;
   }
 }
 
 const circle = new Circle("red", 5);
 console.log(circle.area()); // 输出:78.53981633974483

8.5 静态方法和静态属性

类可以定义静态方法和静态属性。静态方法和静态属性是属于类本身而不是类的实例的。

静态方法是定义在类上的方法,可以直接通过类名来调用,而不需要创建类的实例。静态方法可以访问类的静态属性和其他静态方法,但不能访问类的实例属性和方法。

实例如下:

// 静态属性
class Counter {
  static count: number = 0;
  static increment() {
    Counter.count++;
  }
}

Counter.increment();
Counter.increment();
console.log(Counter.count); // 输出:2

// 静态方法
class MathUtils {
  static add(num1: number, num2: number): number {
    return num1 + num2;
  }
}

console.log(MathUtils.add(2, 3)); // 输出:5

8.6 readonly修饰符

readonly存在于接口和class内部,表明这个属性只读不可修改,类似于Java中final关键字。

8.7 抽象类

java狂喜,抽象类其实就是对类的进一步抽象用于子类继承,不能被实例化。接口和抽象类的区别就是接口不能有成员,是因为它只是对象行为的抽象。当你需要定义一个类型,既包含行为抽象,又包含成员来供子类使用时,显然只有抽象类可以满足了。

抽象类定义如下:

abstract class A{
  // 抽象属性
  abstract name: string;
  // 具体成员
  age: number;
  abstract eat();
  //抽象方法
  protected constructor(age) {
    this.age = age;
  }
}
    
class B extends A{
  name: string;
  constructor(name, age) {
    // 可以直接使用父类的具体成员, 同时又得必须继承父类抽象属相name和抽象方法 eat
    super(age);
    this.name = name;
  }
  eat() {
    console.log("aaaa");
  }
}

8.8 接口继承类

java对此表示不可理解并且骂骂咧咧的离开了。一般的面向对象编程的语言是不能用类来继承接口的,哪能有抽象继承具体的,但是typescript是可以的,鹅妹子嘤。

其实在面向对象设计中的"接口隔离原则"。该原则指出,客户端不应该依赖于它不需要的接口。通过将类的公共部分提取到接口中,我们可以将类的具体实现与客户端代码解耦,从而提高代码的可维护性和可测试性。

另外,接口继承类还可以用于描述类的子类。通过继承类的接口,我们可以定义一组共享的属性和方法,以便在不同的子类中进行实现和重写。就是说你不想过于的去依赖一个父类可以通过一个接口继承它来灵活运用。

写法和接口继承一样,即:

class A{
    // 属性和方法
}
interface B extends A{
    // 属性和方法描述
}

9. 声明合并

相同名字的接口、类、函数会被合并为一个接口、类、函数。

函数重载上面已经说了,其他的则是:

  1. 接口合并:当多个同名接口声明出现时,它们会自动合并为一个接口,合并后的接口包含了所有声明中的属性和方法,内部同名的属性具有唯一性。
  2. 命名空间合并:当多个同名命名空间声明出现时,它们会自动合并为一个命名空间,合并后的命名空间包含了所有声明中的属性、方法和嵌套的命名空间。
  3. 类合并:当一个类同时具有静态属性、静态方法、实例属性和实例方法时,它们会自动合并为一个类,合并后的类包含了所有声明中的属性和方法。

10. 泛型

10.1 泛型定义

简单的来讲就是允许在代码阶段不明确指出所需要的数据类型,而在使用时再指明。它允许我们在定义函数、类或接口时使用参数化类型,以便在使用时指定具体的类型。

泛型的好处之一是可以增加代码的灵活性和重用性。通过使用泛型,我们可以编写更通用的代码,可以在不同的数据类型上进行操作,而不需要重复编写相似的代码。这样可以减少代码的冗余,并提高代码的可维护性和可读性。

举个栗子,当我们需要编写一个函数来处理不同类型的数组时,可以使用泛型来实现。例如,我们可以编写一个函数来计算数组中元素的总和,而不需要关心数组中元素的具体类型。这样,我们可以在使用函数时传入不同类型的数组,而函数会根据传入的数组类型进行相应的计算。

function sum<T>(arr: T[]): T {
 let result: T;
  for (let i = 0; i < arr.length; i++) {
    result += arr[i];
  }
  return result;
}

const numbers = [1, 2, 3, 4, 5];
const total = sum(numbers); // 调用sum函数,并传入数字数组
console.log(total); // 输出15

const strings = ['Hello', 'World'];
const concatenated = sum(strings); // 调用sum函数,并传入字符串数组
console.log(concatenated); // 输出'HelloWorld'

10.2 泛型约束

泛型约束是一种在使用泛型时限制泛型类型的特性。通过泛型约束,我们可以指定泛型类型必须满足某些条件,以确保代码的正确性和安全性。

在 TypeScript 中,我们可以使用泛型约束来限制泛型类型必须具有某些属性或方法。这样,在使用泛型时,我们可以确保泛型类型具有我们所需的特性,从而避免在编译或运行时出现错误。

例如,假设我们有一个函数,需要接收一个泛型类型的参数,并调用该参数的 length 属性来获取其长度。为了确保传入的泛型类型具有 length 属性,我们可以使用泛型约束来限制泛型类型必须是具有 length 属性的类型。

interface Length {
  length: number;
}

function getLength<T extends Length>(value: T): number {
  return value.length;
}
// 当然定义一个类必然是定义了一个类型, 抽象类也是类所以也可以这样写,对于使用次数不高较为简略的约束可以这样避免单独去定义接口
function getLength<T extends {length: number}>(obj: T): number {
  return obj.length;
}

const str = 'Hello';
const arr = [1, 2, 3, 4, 5];

console.log(getLength(str)); // 输出 5
console.log(getLength(arr)); // 输出 5
console.log(getLength(123)); // 编译错误,因为数字类型没有 length 属性

10.3 泛型接口

又是java狂喜的一个内容,当我们需要在多个地方使用相同的类型参数时,可以使用泛型接口。泛型接口允许我们在定义接口时使用类型参数,从而增加代码的灵活性和重用性。在TypeScript中,可以使用尖括号(<>)来指定泛型类型参数,并在接口中使用它们。

例如,我们可以定义一个泛型接口Container,它有一个value属性,类型为泛型类型参数T:

 interface Container<T> {
   value: T;
 }
 const numberContainer: Container<number> = {
   value: 10
 };
 const stringContainer: Container<string> = {
   value: "Hello"
 };

其实在日常开发中我们经常遇见,比如我们最为常用的数组:
前端笔记之typescript基础_第3张图片

11. 声明文件和内置对象

11.1 声明文件

声明文件是用来描述已有代码库的类型信息的文件。在TypeScript中,当我们使用第三方JavaScript库时,由于JavaScript没有类型系统,我们无法获得该库的类型信息。这时,我们可以编写一个声明文件来为该库添加类型信息,以便在TypeScript中使用。

声明文件通常以.d.ts为后缀,可以包含类型声明、接口、命名空间等信息。它们不会被编译成JavaScript,而是在编译时用于类型检查和代码提示。

声明文件有两种主要的使用方式:

  1. 全局声明:用于描述全局变量、全局函数、全局对象等的类型信息。例如,我们可以为jQuery库编写一个声明文件,以便在TypeScript中使用$符号来访问jQuery对象的方法和属性。
  2. 模块声明:用于描述模块的类型信息。当我们使用第三方模块时,可以编写一个声明文件来描述该模块的类型信息,以便在TypeScript中使用模块的导入和导出语法。

编写声明文件时,可以使用TypeScript的类型语法来描述类型信息,包括基本类型、联合类型、交叉类型、泛型等。还可以使用interface、type等关键字来定义接口和类型别名。

声明文件可以通过多种方式获取,包括手动编写、从社区维护的类型库中获取、使用工具自动生成等。TypeScript官方提供了一个类型库管理工具@types,可以通过npm安装第三方库的类型声明文件。

jquery的声明如下:

 // jquery.d.ts
  
declare var $: JQueryStatic;
interface JQueryStatic {
  ajax(settings: JQueryAjaxSettings): void;
  get(url: string, data?: any, success?: any, dataType?: string): void;
  // 其他方法和属性...
}

interface JQuery {
  length: number;
  each(callback: (index: number, element: Element) => any): JQuery;
  // 其他方法和属性...
}

declare function $(selector: string): JQuery;
declare function $(element: Element): JQuery;
declare function $(readyCallback: () => any): JQuery;

11.2 内置对象

在TypeScript中,内置对象的类型信息已经包含在TypeScript的类型定义文件中,因此可以直接使用这些内置对象的类型注解和类型推断。例如,可以使用Array类型来声明一个数组变量,使用Date类型来声明一个日期变量。

总之,内置对象是JavaScript运行环境中自动提供的一些对象,包括全局对象、原生对象和宿主对象。它们提供了常用的功能和接口,可以直接使用。在TypeScript中,可以使用内置对象的类型信息来进行类型注解和类型推断。

12. typescript5.0之后的装饰器使用

java和Python再一次狂喜,特别是Python,使用方法和名称都一样,而在Java中被称为注解。

它允许我们在不修改原始代码的情况下,向函数或者类及其属性添加额外的功能。

12.1 类装饰器

新的类装饰器具有下面的类型签名:

type ClassDecorator = (
  value: Function,
  context: {
    kind: 'class';
    name: string | undefined;
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

我们可以额外从 context 中取到一些信息:

kind:被修饰的结构类型,包括class、method、getter、setter、accessor、field

name:被修饰的实体名称

addInitializer:一个初始化完成后的回调函数,运行的时机会取决于装饰器的种类:

类装饰器:在类被完全定义并且所有静态字段都被初始化之后运行。

非静态类元素装饰器:在实例化期间运行(实例字段被初始化之前)。

静态类元素装饰器:在类定义期间运行(在定义静态字段之前但在定义其他所有其他类元素之后)。

例如:

    /**
     *
     * @param target 被修饰的类
     * @param context 修饰上下文
     * @constructor
     */
    const Log = <T extends { new(...args: any[]): {} }>(target: T, context: ClassDecoratorContext) => {
      console.log(context.kind, context.name)
      context.addInitializer(() => {
        console.log("initializer")
      })
      // 返回一个继承于被修饰类的一个新的类
      return class extends target {
        constructor(...args: any[]) {
          console.log(`Creating instance of ${target.name} with args: ${args}`);
          super(...args);
        }
      }
    }
    
    @Log
    class Example {
      constructor(private name: string) {
      }
      sayHello() {
        console.log(`Hello, ${this.name}!`);
      }
    }
    
    const example = new Example('John');
    example.sayHello();

12.2 类方法装饰器

新的类方法装饰器具有下面的类型签名:

type ClassMethodDecorator = (
   value: Function,
   context: {
     kind: 'method';
     name: string | symbol;
     static: boolean;
     private: boolean;
     access: { get: () => unknown };
     addInitializer(initializer: () => void): void;
   }
 ) => Function | void;

可以发现相比类装饰器,context 中主要多了三个参数:

static:是否威静态方法

private:是否为私有方法

access:可以获取到方法的 getter 方法(通过它我们就可以公开访问私有方法的字段)

  /**
    * 携带前缀参数
    * @param message
    */
   const sign = (message: string) => {
     return (target: any, context: ClassMethodDecoratorContext) => {
       // console.log(target)
       console.log(context)
       console.log('Hi,start sign!' + message);
       return function (this: any, ...args: any[]) {
         console.log(args)
         return target.apply(this, args);
       }
       // console.log(args);
     }
   }
   /**
    * 不携带前缀参数
    * @param target
    * @param context
    */
   const sign2 = (target: any, context: ClassMethodDecoratorContext) => {
     // console.log(target)
     console.log(context)
     console.log('Hi,start sign!');
     return function (this: any, ...args: any[]) {
       console.log(args)
       return target.apply(this, args);
     }
     // console.log(args);
   }
   
   class People {
     constructor(private _name: string) {
     };
   
     @sign("bingo")
     getName(str) {
       console.log(this._name, str)
     }
   }
   
   const p = new People("lee");
   p.getName("enenen")

12.3 类属性装饰器

新的类属性装饰器具有下面的类型签名:

type ClassFieldDecorator = (
   value: undefined,
   context: {
     kind: 'field';
     name: string | symbol;
     static: boolean;
     private: boolean;
     access: { get: () => unknown, set: (value: unknown) => void };
     addInitializer(initializer: () => void): void;
   }
 ) => (initialValue: unknown) => unknown | void;

相比类方法装饰器,主要有下面两个地方不同:

access 中可以同时拿到 setter 和 getter 方法,但是类方法装饰器只能拿到 getter 方法。

返回值类型不同,在类属性装饰器中,可以通过返回一个方法来改变属性的初始值 initialValue

例如:

const changeInitValue = (value: undefined, context: ClassFieldDecoratorContext) => {
   if(context.kind === "field"){
     return function (initialValue){
       console.log(`get initialValue, it is ${initialValue}.`)
       return initialValue * 2;
     }
   }
 }
class C {
   @changeInitValue
   public field = 5;
 }
 const temp = new C();

12.4 自动访问器

装饰器提案引入了一个新的语言特性:

auto accessor (自动访问器),我们可以通过将 accessor 关键字放在类字段之前来创建自动访问器,当没有装饰器的时候,它和其他普通的使用起来是一样的。

 class People {
     accessor name = 'ConardLi';
    }
    
    //其实就等同于下面的代码:
    class People {
     name = 'ConardLi';
     get name() {
      return this.name;
     }
    
     set name(value) {
      this.name = value;
     }   
 }

看起来没有什么区别,但是当我们使用装饰器来修饰一个 accessor 字段时,它的用处就大了,它的类型签名是这样的:

type ClassAutoAccessorDecorator = (
    value: {
     get: () => unknown;
     set: (value: unknown) => void;
    },
    context: {
     kind: 'accessor';
     name: string | symbol;
     static: boolean;
     private: boolean;
     access: { get: () => unknown, set: (value: unknown) => void };
     addInitializer(initializer: () => void): void;
    }
) => {
    get?: () => unknown;
    set?: (value: unknown) => void;
    init?: (initialValue: unknown) => unknown;
} | void;

案例如下:

 const autoAccessor =( value: {
   get: () => unknown;
   set: (value: unknown) => void;
 }, constext: ClassAccessorDecoratorContext) =>{
   if(constext.kind === "accessor"){
     const {set, get} = value;
     return {
       get(){
         console.log("获取属性");
         return get.call(this);
       },
       set(v: unknown){
         console.log(v);
         console.log(`注入属性值${v}`);
         set.call(this, v);
       }
     }
   }
 }
 class AccessorTest {
   @autoAccessor
   accessor name;
   constructor(name){
     this.name = name;
   }
 }
 
 const at = new AccessorTest("haha");
 console.log(at.name);

12.5 getter、setter装饰器

accessor 是 帮我们生成getter和setter,但是有的时候我们只想在 getter 或者 setter 的时机去做一些事情,新的装饰器还具有直接修饰 getter 和 setter 的能力,它的类型签名如下:

    type ClassGetterDecorator = (
      value: Function,
      context: {
        kind: 'getter';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { get: () => unknown };
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;
    
    type ClassSetterDecorator = (
      value: Function,
      context: {
        kind: 'setter';
        name: string | symbol;
        static: boolean;
        private: boolean;
        access: { set: (value: unknown) => void };
        addInitializer(initializer: () => void): void;
      }
    ) => Function | void;

用例参考accessor。

你可能感兴趣的:(前端,前端,笔记,typescript)