Typescript技术分享

1、初识TypeScript

TypeScript是什么?

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集。TypeScript 在 JavaScript 的基础上添加了可选的静态类型和基于类的面向对象编程。

2、TS类型

2.1 布尔类型(boolean)

boolean类型只有两个取值:true和false,非常简单

const flag: boolean = true;

2.2 Number 类型
const flag: number = 1;

2.3 String 类型
const flag: string = "hello";

2.4 数组类型(array)和Object类型
数组类型
const flag1: number[] = [1, 2, 3];
const flag2: Array<number> = [1, 2, 3];

object类型
const myInfo: object = {
    name: "why",
    age: 18,
    height: 1.88
}

2.5 null 和 undefined

undefined 和 null 两者有各自的类型分别为 undefined 和 null

let u: undefined = undefined;
let n: null = null;

2.6 Enum 类型

使用枚举我们可以很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及可以用它来描述一些状态信息,比如错误码等

// 普通枚举 初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标
enum Color {
  RED,
  PINK,
  BLUE,
}

const pink: Color = Color.PINK;
console.log(pink); // 1

// 设置初始值
enum Color {
  RED = 10,
  PINK,
  BLUE,
}
const pink: Color = Color.PINK;
console.log(pink); // 11

// 字符串枚举 每个都需要声明
enum Color {
  RED = "红色",
  PINK = "粉色",
  BLUE = "蓝色",
}

const pink: Color = Color.PINK;
console.log(pink); // 粉色

// 常量枚举 它是使用 const 关键字修饰的枚举,常量枚举与普通枚举的区别是,整个枚举会在编译阶段被删除 我们可以看下编译之后的效果

const enum Color {
  RED,
  PINK,
  BLUE,
}

const color: Color[] = [Color.RED, Color.PINK, Color.BLUE];

//编译之后的js如下:
var color = [0 /* RED */, 1 /* PINK */, 2 /* BLUE */];
// 可以看到我们的枚举并没有被编译成js代码 只是把color这个数组变量编译出来了

2.7 元组类型(tuple)

元祖是一种特殊的数组,元祖类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为string和number类型的元祖。元祖在实际开发中使用的非常少

//声明一个元祖类型
let x : [string,number]
//正确的初始化
x = ['hello',10]
//错误的初始化方法
x = [10,'hello']

2.8 任意类型(any)

任何类型都可以被归为 any 类型 这让 any 类型成为了类型系统的 顶级类型 (也被称作 全局超级类型)

TypeScript 允许我们对 any 类型的值执行任何操作 而无需事先执行任何形式的检查

一般使用场景:

第三方库没有提供类型文件时可以使用 any

类型转换遇到困难或者数据结构太复杂难以定义

不过不要太依赖 any 否则就失去了 ts 的意义了

const flag: any = document.getElementById("root");

2.9 Symbol

在ES5中,如果我们是不可以在对象中添加相同的属性名称的,比如下面的做法:

通常我们的做法是定义两个不同的属性名字:比如identity1和identity2。

但是我们也可以通过symbol来定义相同的名称,因为Symbol函数返回的是不同的值:

const sym1 = Symbol("title");
const sym2 = Symbol("title");
const person = {
    [sym1]: "程序员",
    [sym2]: "老师"
}

2.10 Unknown 类型

unknown 和 any 的主要区别是 unknown 类型会更加严格 在对 unknown 类型的值执行大多数操作之前 我们必须进行某种形式的检查 而在对 any 类型的值执行操作之前 我们不必进行任何检查

所有类型都可以被归为 unknown 但unknown类型只能被赋值给 any 类型和 unknown 类型本身 而 any 啥都能分配和被分配

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error

2.11 void 类型

void 表示没有任何类型 当一个函数没有返回值时 TS 会认为它的返回值是 void 类型。声明一个void类型的变量没有什么大用,因为只能为它赋予undefined和null,也就是函数可以返回null或者undefined

function hello(name: string): void {}

2.12 never 类型

never 一般表示用户无法达到的类型 例如never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

function neverReach(): never {
  throw new Error("an error");
}

never 和 void 的区别
void 可以被赋值为 null 和 undefined 的类型。 never 则是一个不包含值的类型。
拥有 void 返回值类型的函数能正常运行。拥有 never 返回值类型的函数无法正常返回,无法终止,或会抛出异常。

2.13 联合类型

联合类型是由两个或者多个其他类型组成的类型

联合类型中的每一个类型被称之为联合成员(union's members);

function printId(id: number | string ) {
    console.log("你的id是:"id)
}
printId(10)
printId("abc")

2.14 类型推论

指编程语言中能够自动推导出值的类型的能力 它是一些强静态类型语言中出现的特性 定义时未赋值就会推论成 any 类型 如果定义的时候就赋值就能利用到类型推论

let flag; //推断为any
let count = 123; //为number类型
let hello = "hello"; //为string类型

2.15 类型断言

有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)

类型断言有两种形式
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
以上两种方式虽然没有任何区别,但是尖括号格式会与 react 中 JSX 产生语法冲突,因此我们更推荐使用 as 语法。

非空断言

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测,可以用于断言操作对象是非 null 和非 undefined 类型

let flag: null | undefined | string;
flag!.toString(); // ok
flag.toString(); // error

2.16 字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

let flag1: "hello" = "hello";
let flag2: 1 = 1;
let flag3: true = true;

2.17 类型别名

类型别名用来给一个类型起个新名字

type flag = string | number;
function hello(value: flag) {}

2.18 交叉类型

交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

type Flag1 = { x: number };
type Flag2 = Flag1 & { y: string };

let flag3: Flag2 = {
  x: 1,
  y: "hello",
  henb,
};

2.19 类型保护

类型保护就是一些表达式,他们在编译的时候就能通过类型信息确保某个作用域内变量的类型 其主要思想是尝试检测属性、方法或原型,以确定如何处理值

typeof 类型保护
function double(input: string | number | boolean) {
  if (typeof input === "string") {
    return input + input;
  } else {
    if (typeof input === "number") {
      return input * 2;
    } else {
      return !input;
    }
  }
}

in 关键字

用于确定对象是否具有带名称的属性:in运算符

如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;

interface Bird {
  fly: number;
}

interface Dog {
  leg: number;
}

function getNumber(value: Bird | Dog) {
  if ("fly" in value) {
    return value.fly;
  }
  return value.leg;
}

instanceof 类型保护

用来检查一个值是否是另一个值的“实例”

class Animal {
  name!: string;
}
class Bird extends Animal {
  fly!: number;
}
function getName(animal: Animal) {
  if (animal instanceof Bird) {
    console.log(animal.fly);
  } else {
    console.log(animal.name);
  }
}

2.20 可选链的使用

可选链使用可选链操作符 ?.

它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行

2.21 ??和!!的作用

!!操作符:

将一个其他类型转换成boolean类型;

类似于Boolean(变量)的方式

??操作符:

空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数

3 函数

3.1 函数的定义

比如现在我们有个找小姐姐的需求:

找18岁的小姐姐

找28岁的小姐姐

找38岁的小姐姐

我们可以把功能相近的需求封装成一个独立的代码块,每次传入不同的变量或参数,就可以实现不同的结果。

可以指定参数的类型和返回值的类型

function searchXiaoJieJie(age:number):string{
    return '找到了'+age+'岁的小姐姐' 
}
var age:number = 18
var result:string = searchXiaoJieJie(age)
console.log(result)

3.2 三种函数表达式

函数声明法

function add(n1:number,n2:number):number{
    return n1+n2
}

函数表达式法

函数表达式法是将一个函数赋值给一个变量,这个变量名就是函数名。通过变量名就可以调用函数了。这种方式定义的函数,必须在定义之后,调用函数。下面例子中等号右边的函数没有函数名,称为匿名函数。

var add = function(n1:number,n2:number):number{
    return n1+n2
}
console.log(add(1,4))

箭头函数

箭头函数是 ES6 中新增的函数定义的新方式,我们的 TypeScript 语言是完全支持 ES6 语法的。箭头函数定义的函数一般都用于回调函数中。

var add = (n1:number,n2:number):number=>{
    return n1+n2
}

console.log(add(1,4))

3.3 可选参数

可选参数,就是我们定义形参的时候,可以定义一个可传可不传的参数。这种参数,在定义函数的时候通过?标注。

比如我们继续作找小姐姐的函数,这回不仅可以传递年龄,还可以选择性的传递身材。我们来看如何编写。

function searchXiaoJieJie2(age:number,stature?:string):string{
    let yy:string = ''
    yy = '找到了'+age+'岁'
    if(stature !=undefined){
        yy = yy + stature
    }
    return yy+'的小姐姐'
}
var result:string  =  searchXiaoJieJie2(22,'大长腿')
console.log(result)

3.4 默认参数

有默认参数就更好理解了,就是我们不传递的时候,他会给我们一个默认值,而不是undefined了。我们改造上边的函数,也是两个参数,但是我们把年龄和身材都设置默认值

function searchXiaoJieJie2(age:number=18,stature:string='大胸'):string{
    let yy:string = ''
    yy = '找到了'+age+'岁'
    if(stature !=undefined){
        yy = yy + stature
    }
    return yy+'的小姐姐'
}

var result:string  =  searchXiaoJieJie2()
console.log(result)

3.5 剩余参数

有时候我们有这样的需求,我传递给函数的参数个数不确定。例如:我找小姐姐的时候有很多要求,个人眼光比较挑剔。这时候你不能限制我,我要随心所欲。

说的技术点,剩余参数就是形参是一个数组,传递几个实参过来都可以直接存在形参的数组中。

function searchXiaoJieJie3(...xuqiu:string[]):string{
    let  yy:string = '找到了'
    for (let i =0;i<xuqiu.length;i++){
        yy = yy + xuqiu[i]
        if(i<xuqiu.length){
            yy=yy+'、'
        }
    }
    yy=yy+'的小姐姐'
    return yy

}
var result:string  =  searchXiaoJieJie3('22岁','大长腿','瓜子脸','水蛇腰')
console.log(result)

3.6 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 在 TypeScript 中,表现为给同一个函数提供多个函数类型定义

在我们调用attr的时候,它会根据我们传入的参数类型来决定执行函数体时,到底执行哪一个函数的重载签名;

let obj: any = {};
function attr(val: string): void;
function attr(val: number): void;
function attr(val: any): void {
  if (typeof val === "string") {
    obj.name = val;
  } else {
    obj.age = val;
  }
}
attr("hahaha");
attr(9);
attr(true);
console.log(obj);

注意:函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型

4 类

4.1 类的定义

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类,并在里边声明了name和age属性。constructor为构造函数。构造函数的主要作用是给类中封装的属性进行赋值。

class Person {
  name!: string; //如果初始属性没赋值就需要加上!
  constructor(_name: string) {
    this.name = _name;
  }
  getName(): void {
    console.log(this.name);
  }
}
let p1 = new Person("hello");
p1.getName();

当然 如果我们图省事 我们也可以把属性定义直接写到构造函数的参数里面去(不过一般不建议这样写 因为会让代码增加阅读难度)

class Person {
  constructor(public name: string) {}
  getName(): void {
    console.log(this.name);
  }
}
let p1 = new Person("hello");
p1.getName();

注意:当我们定义一个类的时候,会得到 2 个类型 一个是构造函数类型的函数类型(当做普通构造函数的类型) 另一个是类的实例类型(代表实例)

class Component {

  static myName: string = "静态名称属性";

  myName: string = "实例名称属性";

}

//ts 一个类型 一个叫值

//放在=后面的是值

let com = Component; //这里是代表构造函数

//冒号后面的是类型

let c: Component = new Component(); //这里是代表实例类型

let f: typeof Component = com;

4.2 存取器(setter/getter)

在 TypeScript 中,我们可以通过存取器来改变一个类中属性的读取和赋值行为

class Person {
  private _name: string
  constructor(name: string) {
    this._name = name
  }
  // 访问器setter/getter
  // setter
  set name(newName) {
    this._name = newName
  }
  // getter
  get name() {
    return this._name
  }
}
const p = new Person("why")
p.name = "coderwhy"
console.log(p.name) // coderwhy
export {}

4.3 readonly 只读属性

readonly 修饰的变量只能在构造函数中初始化

class Person {
  // 1.只读属性是可以在构造器中赋值, 赋值之后就不可以修改
  readonly name: string
  constructor(name: string) {
    this.name = name
  }
}
const p = new Person("why")
console.log(p.name) // why
// p.name = "123" 不能直接修改

4.4 继承

子类继承父类后子类的实例就拥有了父类中的属性和方法,可以增强代码的可复用性

将子类公用的方法抽象出来放在父类中,自己的特殊逻辑放在子类中重写父类的逻辑

super 可以调用父类上的方法和属性

在 TypeScript 中,我们可以通过 extends 关键字来实现继

class Person {
  name: string; //定义实例的属性,默认省略public修饰符
  age: number;
  constructor(name: string, age: number) {
    //构造函数
    this.name = name;
    this.age = age;
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}
class Student extends Person {
  no: number;
  constructor(name: string, age: number, no: number) {
    super(name, age);
    this.no = no;
  }
  getNo(): number {
    return this.no;
  }
}
let s1 = new Student("hello", 10, 1);
console.log(s1);

4.5 类里面的修饰符

访问修饰符分为:public、protected、private。

public:公有修饰符,可以在类内或者类外使用public修饰的属性或者行为,默认修饰符。

class Person {
  public name: string = "ddd"
}
const p = new Person()
console.log(p.name) //ddd

protected:受保护的修饰符,可以本类和子类中使用protected修饰的属性和行为。

// protected: 在类内部和子类中可以访问
class Person {
  protected name: string = "123"
}
class Student extends Person {
  getName() {
    return this.name
  }
}
const stu = new Student()
console.log(stu.getName()) // 123

private : 私有修饰符,只可以在类内使用private修饰的属性和行为。

class Person {
  private name: string = ""

  // 封装了两个方法, 通过方法来访问name
  getName() {
    return this.name
  }

  setName(newName: string) {
    this.name = newName
  }
}

const p = new Person()
console.log(p.getName()) // why
p.setName("why")

export {}

4.6 静态属性 静态方法

类的静态属性和方法是直接定义在类本身上面的 所以也只能通过直接调用类的方法和属性来访问

 //注意静态方法里面的this指向的是类本身 而不是类的实例对象 所以静态方法里面只能访问类的静态属性和方法
class Student {
  static time: string = "20:00"
  static attendClass() {
    console.log("去学习~")
  }
}
console.log(Student.time) //20:00
Student.attendClass() //去学习~

4.7 重写是指子类重写继承自父类中的方法 重载是指为同一个函数提供多个类型定义
class Animal {
  speak(word: string): string {
    return "动物:" + word;
  }
}
class Cat extends Animal {
  speak(word: string): string {
    return "猫:" + word;
  }
}
let cat = new Cat();
console.log(cat.speak("hello"));
// 上面是重写
//--------------------------------------------
// 下面是重载
function double(val: number): number;
function double(val: string): string;
function double(val: any): any {
  if (typeof val == "number") {
    return val * 2;
  }
  return val + val;
}

let r = double(1);
console.log(r);

4.8 类的多态
class Animal {
  action() {
    console.log("animal action")
  }
}

class Dog extends Animal {
  action() {
    console.log("dog running!!!")
  }
}

class Fish extends Animal {
  action() {
    console.log("fish swimming")
  }
}
// animal: dog/fish
// 多态的目的是为了写出更加具备通用性的代码
function makeActions(animals: Animal[]) {
  animals.forEach(animal => {
    animal.action()
  })
}
makeActions([new Dog(), new Fish()]) //dog running!!! fish swimming

5 接口

接口既可以在面向对象编程中表示为行为的抽象,也可以用来描述对象的形状

我们用 interface 关键字来定义接口 在接口中可以用分号或者逗号分割每一项,也可以什么都不加

5.1 对象的形状
//接口可以用来描述`对象的形状`
//接口可以用来描述`对象的形状`
interface Speakable {
  speak(): void;
  readonly lng: string; //readonly表示只读属性 后续不可以更改
  name?: string; //?表示可选属性
}

let speakman: Speakable = {
  //   speak() {}, //少属性会报错
  name: "hello",
  lng: "en",
  age: 111, //多属性也会报错
};

5.2 行为的抽象

接口可以把一些类中共有的属性和方法抽象出来,可以用来约束实现此接口的类

一个类可以实现多个接口,一个接口也可以被多个类实现

我们用 implements关键字来代表 实现

//接口可以在面向对象编程中表示为行为的抽象
interface Speakable {
  speak(): void;
}
interface Eatable {
  eat(): void;
}
//一个类可以实现多个接口
class Person implements Speakable, Eatable {
  speak() {
    console.log("Person说话");
  }
  //   eat() {} //需要实现的接口包含eat方法 不实现会报错
}

5.3 定义任意属性

如果我们在定义接口的时候无法预先知道有哪些属性的时候,可以使用 [propName:string]:any,propName 名字是任意的

interface Person {
  id: number;
  name: string;
  [propName: string]: any;
}

let p1 = {
  id: 1,
  name: "hello",
  age: 10,
};
这个接口表示 必须要有 id 和 name 这两个字段 然后还可以新加其余的未知字段

5.4 接口的继承

我们除了类可以继承 接口也可以继承 同样的使用 extends关键字

interface Speakable {
  speak(): void;
}
interface SpeakChinese extends Speakable {
  speakChinese(): void;
}
class Person implements SpeakChinese {
  speak() {
    console.log("Person");
  }
  speakChinese() {
    console.log("speakChinese");
  }
}

5.5 函数类型接口

可以用接口来定义函数类型

interface ISwim {
  swimming: () => void
}

interface IEat {
  eating: () => void
}

// 类实现接口
class Animal {
  
}

// 继承: 只能实现单继承
// 实现: 实现接口, 类可以实现多个接口
class Fish extends Animal implements ISwim, IEat {
  swimming() {
    console.log("Fish Swmming")
  }
  eating() {
    console.log("Fish Eating")
  }
}
class Person implements ISwim {
  swimming() {
    console.log("Person Swimming")
  }
}
// 编写一些公共的API: 面向接口编程
function swimAction(swimable: ISwim) {
  swimable.swimming()
}
// 1.所有实现了接口的类对应的对象, 都是可以传入
swimAction(new Fish())
swimAction(new Person())


swimAction({swimming: function() {}})
export {}

6 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性

为了更好的了解泛型的作用 我们可以看下面的一个例子

function createArray(length: number, value: any): any[] {
  let result = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}
createArray(3, "x"); // ['x', 'x', 'x']

上述这段代码用来生成一个长度为 length 值为 value 的数组

但是我们其实可以发现一个问题 不管我们传入什么类型的 value 返回值的数组永远是 any 类型 如果我们想要的效果是 我们预先不知道会传入什么类型 但是我们希望不管我们传入什么类型 我们的返回的数组的指里面的类型应该和参数保持一致 那么这时候 泛型就登场了

使用泛型改造

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}
createArray<string>(3, "x"); // ['x', 'x', 'x']

我们可以使用<>的写法 然后再面传入一个变量 T 用来表示后续函数需要用到的类型 当我们真正去调用函数的时候再传入 T 的类型就可以解决很多预先无法确定类型相关的问题

6.1 多个类型参数

如果我们需要有多个未知的类型占位 那么我们可以定义任何的字母来表示不同的类型参数

interface ILength {
  length: number
}
function getLength<T extends ILength>(arg: T) {
  return arg.length
}
getLength("abc")
getLength(["abc", "cba"])
getLength({length: 100})

6.2 泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

6.3 泛型接口

定义接口的时候也可以指定泛型

interface Cart<T> {
  list: T[];
}
let cart: Cart<{ name: string; price: number }> = {
  list: [{ name: "hello", price: 10 }],
};
console.log(cart.list[0].name, cart.list[0].price); //hello 10

我们定义了接口传入的类型 T 之后返回的对象数组里面 T 就是当时传入的参数类型

6.4 泛型类
class MyArray<T> {
  private list: T[] = [];
  add(value: T) {
    this.list.push(value);
  }
  getMax(): T {
    let result = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
      if (this.list[i] > result) {
        result = this.list[i];
      }
    }
    return result;
  }
}
let arr = new MyArray();
arr.add(1);
arr.add(2);
arr.add(3);
let ret = arr.getMax();
console.log(ret); // 3
上诉例子我们实现了一个在数组里面添加数字并且获取最大值的泛型类

6.5 泛型类型别名
type Cart<T> = { list: T[] } | T[];
let c1: Cart<string> = { list: ["1"] };
let c2: Cart<number> = [1];

6.6 泛型参数的默认类型

我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值