【TS】关于学习coderwhy&TS课程中的随笔

关于学习coderwhy&TS课程中的随笔

这篇文章是本人在学习coderwhy的TS课程时随手记录而成,王老师的课虽价格不菲但着实细致入微,目前全网应该没有人或机构在前端教学这块可以与coderwhy的课媲美了(并非硬吹,这是我作为一个小白在学习过某马某硅谷某锋推出的课程后有感而发)。奈何有些内容实在太深奥,对初学者确实不友好,比如TS中的内置工具的使用、类型体操实现内置工具等内容简直让人叹为观止,后续我会继续更新课程中涉及到类型体操的内容。

——邂逅TS语法

  • 声明一个标识符时,如果直接进行赋值,会根据赋值的类型推导出标识符的类型注解,这个过程就称为类型推导

  • 用let定义进行的类型推导,推导出来的是通用类型

    用const定义进行的类型推导,推导出来的是字面量类型

——TS数据类型

  • 明确的指定数组的类型注解有两种写法:

    1.string[] 表示元素都是字符串类型的数组

    2.Array 表示元素都是数字类型的数组

    注意事项:在开发中,数组中一般存放相同的类型

  • 在定义一个TS中的函数时,要明确指定参数的类型

  • 在定义TS中的函数时,返回值的类型可以明确指定,也可以进行类型推导

  • 匿名函数最好不要添加类型注解,会根据上下文自动推导

    names.forEach(function (item, index, arr) {
        console.log(item, index, arr);
    })
    

    TS会根据forEach函数的类型以及数组的类型推断出item的类型,这个过程称为上下文类型

  • type PointType = {
        x: number,
        y: number,
        z?: number//表示z为可有可无的属性
    }
    
  • any类型表示不限制标识符的任意类型,并且可以在标识符上面进行任何操作,有点类似TS回到了JS

  • unknown类型默认在标识符上面进行任何的操作都是非法的,都会报错

    要求再进行任何的操作之前都要进行类型校验(缩小),才能根据缩小后的类型进行对应操作

    unknown类型和any类型有点相似,但是在unknown类型的值上进行任何操作都是不合法的

    let foo:unknown='aaa'
    foo=123
    if(typeof foo==='string'){//类型缩小
        console.log(foo.length);
    }
    
  • 在TS中,如果一个函数没有标明任何的返回值(连undefined都没有返回),那么返回值的类型就是void类型

    function sum(num1:number,num2:number):void{
        console.log(num1+num2);
        // return num1+num2
    }
    

    如果一个函数的返回值类型是void类型,那么我们可以return undefined

  • void的应用场景:用来指定函数类型的返回值是void

    type FooType=()=>void
    const foo:FooType=()=>{
        
    }
    
  • 基于上下文类型推导的函数中的返回值是void类型,不强制要求不能返回任何的东西:

    const names=['abc','cba','nba']
    names.forEach((item,index,arr)=>{
        //该回调函数的返回值类型是void,但是我可以return回去一个其他的东西
        return 123
    })
    
  • 实际开发中只有进行类型推导时,可能会自动推导出(比如抛出错误)是never类型,但是很少使用

    function foo():never{
        throw new Error('123')
    }
    

    封装框架、工具库的时候可以使用一下never:

    在扩展工具的时候,对于一些没有处理的case,可以直接报错:

    function handleMessage(message:string|number|boolean){
        switch(typeof message){
            case 'string':
                console.log(message.length)
                break
            case 'number':
                console.log(message)
                break
            case 'boolean':
                console.log(message)
                break
            default:
                const check:never=message
        }
    }
    
  • 数组中的所有元素最好是相同的数据类型,如果获取值之后不明确知道对应的的数据类型则不适合用数组保存

    const info3:[string,number,number]=['why',18,1.88]
    

    元组结构中可以存放不同的数据类型,取出来的每一项也是有明确的类型

    const info:[string,number,number]=['why',188,1.8]
    

    在函数中使用元组类型是最多的:

    function useState(initalState:number):[number,(newState:number)=>void]{
    	let stateValue=initalState
        function setValue(newValue:number){
    		setValue=newValue
        	}
         return [stateValue,setValue]      
    }
    

——TS语法细节

  • type就是用来起别名的

    通过type来声明一个对象类型,对象的另一种声明方式是使用inerface来声明

  • 别名和接口的区别:

    1. type类型适用范围更广,接口类型只能用来声明对象

    2. 在声明对象时,interface对于同一个接口名称可以多次声明,而type不可以

    3. interface支持继承

      interface IPerson{
          name:string,age:number
      }
      interface IKun extends IPerson{
          kouhao:string
      }
      
    4. interface可以被类实现(TS面向对象)

      class Person implements IPerson{
           
      }
      
    5. 总结:非对象类型的定义使用type、对象类型的声明使用interface

  • 有时候使用TS无法获取具体的类型信息,这个时候需要使用类型断言

    比如使用document.getElementById,TS只知道该函数返回HTMLElement,但不知道它的具体类型

    const myIMG=docuumen.getElemenById('myimg') as HTMLImageElement
    myIMG.src='图片地址'
    
  • 类型断言的规则

    TS只允许类型断言转换为更具体或者不太具体的类型版本,这样可以防止不可能的强制类型转换

    const age:number=18
    const age2=age as  any
    const age3=age2 as string
    
  • 访问属性可选链: ?

    访问一个可能为undefined的属性的时候可以使用可选链

    console.log(info.friend?.name)
    
  • 当确定一个属性不为udefined时,可以直接使用非空类型断言判定不为undefined(也可以使用类型缩小)

    info.friend!.name='james'
    

    非空类型断言有些危险,只有在确保friend有值的情况下才可以使用

  • 类型缩小/类型收窄

    通过类似于typeof padding === 'number' 的判断语句来改变TS的执行路径

    在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称为缩小

  • 常见的类型保护有:

    1. typeof

    2. 平等缩小(=、!

      type Direction='left'|'up'|'right'|'down'
      function switchDirection(direction:Direction){
          if(direction==='left'){
              console.log('向左移动')
          }
      }
      
    3. instanceof

      function printDate(date:string|Date){
          if(date instanceof Date){
              console.log(date.getTime())
          }else{
              console.log(date)
          }
      }
      
    4. in,用于确认对象是否有带名称的属性,如果指定的属性在指定的对象或者其原型链中,则in返回true

——TS函数类型

  • 函数类型的表示方法:

    1. 可以编写函数类型表达式来表示函数类型

      type BarType=(num1:number)=> number
      const bar:BarType=(arg)=>{
          return 123
      }
      

      注意:TS对传入函数的函数类型的参数个数不进行检测

      TS对于很多类型的检测都不报错,这取决于它的内部规则

      当对象是第一次定义的时候,TS会对其保持新鲜感、要求定义的对象必须和要求保持一致,但如果将定义好的对象赋值,此时TS认为该对象已经不新鲜,就不会对其进行类型检测了

    2. 函数类型表达式并不能支持声明函数作为对象所拥有的属性,如果要描述一个带属性的函数,可以在一个对象类型中写一个调用签名

      interface IBar{
          name:string,
          age:number,
          //(函数本身的参数列表):函数的返回值类型
          (num1:number):number
      }
      
      const bar:IBar=(num1:number)=>{
          return 123
      }
      bar.name='777'
      bar.age=13
      

      开发中如何选择:

      1. 如果只是描述函数类型本身,就是用函数类型表达式
      2. 如果要描述函数作为对象时可以被调用,同时也有其他属性时,使用函数调用签名
    3. 描述一个构造函数时,使用构造签名

      class Person{
      
      }
      
      
      interface CTOR{
          new ():Person
      }
      
      
      function factory(fn:CTOR){
          const f=new fn()
          return f
      }
      
      factory(Person)
      
  • 函数的参数:

    1. 函数的可选参数的类型并非只有一种:

      function foo(x:number,y?:number){//y的类型为number|nudefined的联合类型
      	
      }
      foo(1)
      
    2. 函数的参数可以有默认值:

      • 有默认值的情况下,参数的类型注解可以省略

        function foo(x:number,y=100){//y的类型为number|nudefined的联合类型
        	
        }
        
      • 有默认值的参数传参时可以传入undefined

        function foo(x:number,y=100){
        
        }
        foo(1,undefined)
        
    3. 函数的剩余参数:

      当传入的参数数量不定、类型不定时:

      function foo(...args:(string|number)[]){
      
      }
      foo(1,2,3,4,5)
      foo('aaa','bbb')
      
  • TS中的this类型

    1. 在没有对TS进行特殊配置的情况下,this是any类型的
    2. 当在终端执行tsc --init初始化一个ts配置文件后,VScode就会根据配置文件解析ts文件,当我们在该配置文件中设置noImplicitThis为true时,TS会根据上下文推导this的类型,但是在不能正确推导时就会报错

    因此在开启noImplicitThis的情况下,必须指定this的类型:

    1. 函数的第一个参数用于声明this的类型

    2. 在后续调用函数传参时,从第二个参数开始传递,形参中的this参数在编译后会被抹除

      function foo(this:{name:string},info:{name:string}){
      	console.log(this.name)    
      }
      //调用时不需要传入this
      foo.call({name:'why'},{name:'kobe'})
      
  • TS中this类型相关的工具:

    1. 获取函数中this的类型——ThisParameterType

      function foo(this:{name:string},info:{name:string}){
          
      }
      type FooType=typeof foo
      //获取FooType中this的类型:
      type FooThisType=ThisParameterType<FooType>
      
    2. 抹除一个函数类型Type的this参数类型,并返回当前的函数类型——OmitThisParameter

      function foo(this:{name:string},info:{name:string}){
          
      }
      type FooType=typeof foo
      //删除this参数类型,剩余的函数类型:
      type PureFooType=OmitThisParameter<FooType>
      
    3. 用来绑定一个上下文的this——ThisType

      //太抽象了。。。没听懂暂时搁置
      
  • 在TS中,可以编写不同的重载签名来表示函数可以通过不同的方式进行调用

    一般是编写若干个重载签名,再去编写一个通用函数进行实现

    // 编写若干个重载签名,重载签名没有实现体
    function add(arg1:number,arg2:number):number
    function add(arg1:string,arg2:string):string
    // 通用函数:
    function add(arg1:any,arg2:any):any{
        return arg1+arg2
    }
    add(1,1)
    add('a','b')
    add(1,'aa')//会报错
    

——TS面向对象

  • 类可以有自己的构造函数,当new关键字创建实例时,构造函数就会被调用;构造函数不需要任何返回值,默认返回当前创建出来的实例

    class Person {
    
        // 在ts中,类的成员属性是需要声明的
        // name:string
        // age:number
    
        // 或者直接赋一个初始值
        name = ''
        age = 0
    
        constructor(name: string, age: number) {
            this.name = name, this.age = age
        }
    }
    
  • 面向对象的一大特性就是继承,继承也是多态的使用前提

    class Student extends Person{
        constructor(name:string,age:number,sno:number){
            super(name,age)
            this.sno=sno
        }
    }
    
  • 在TS中,类的属性和方法支持三种修饰符:publicprivateprotected

    class Person {
        // 修饰符public修饰的是任何地方可见、公开的属性或方法,默认编写的属性和方法都为public
        // 修饰符protected修饰的是仅在类自身以及子类自身中可见、可访问的属性或方法
        // 修饰符private会将公共方法变为私有方法,私有方法只能在类的内部使用
        protected name: string
        public age: number
        constructor(name: string, age: number) {
            this.name = name
            this.age = age
        }
    
        private eating() {
        }
    }
    const p = new Person('hy', 18)
    // p.name='kobe'    实例不可以访问protected修饰的属性
    // p.eating()   实例不可以访问private修饰的私有属性
    class Ctudents extends Person {
        study() {
            console.log(this.name);//子类可以访问到protected修饰的属性
        }
    }
    

    语法糖:

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

    这种写法可以简化为:

    class Person{
    	constructor(public name:string,public age:number){
            
        }
    }
    
  • setter/getter:对属性的访问和修改进行拦截操作

    class Person {
        private _name: string
        private _age: number
        constructor(name: string, age: number) {
            this._name = name
            this._age = age
    
        }
        running() {
            console.log(this._name);
        }
        // setter/getter:对属性的访问和修改进行拦截过滤
        get name() {
            return this._name
        }
        set name(newValue) {
            this._name = newValue
        }
        get age() {
            return this._age
        }
        set age(newValue) {
            if (newValue > 0 && newValue < 100) {
                this._age = newValue
            }
        }
    }
    const p = new Person('why', 19)
    console.log(p.age);//输出19
    p.age = 300
    console.log(p.age);//依然输出19
    p.age = 50
    console.log(p.age);//输出50
    
  • TS提供的语法糖——参数属性:

    class Person{
        //此时的public必须写!!!
        constructor(public name:string,private _age:number,readonly heght:number){
            
        }
    }
    const p=new Person('why',18,188)
    
  • TS在进行类型检测时候使用的是鸭子类型

    不关心你是不是对应的类型,只关心你有没有要求的属性和方法

    class Person {
        constructor(public name: string, public age: number) {
        }
    }
    class Dog {
        constructor(public name: string, public age: number) {
        }
        running() { }
    }
    function printf(p: Person) {
        console.log(p.name);
    }
    printf(new Person('why', 19))
    printf({ name: '', age: 10 })
    printf(new Dog('dog', 10))
    const person: Person = new Dog('aaa', 1)
    export { } 
    
  • 定义对象类型时可用到的修饰符:

    type IPerson = {
        // 修饰符?——可选属性
        name?: string
        // 修饰符readonly——只读属性
        readonly age: number
    }
    
  • TS中接口的继承特性

    interface IPerson {
        name:string,
        age:number
    }
    interface Ikun extends IPerson{
        slogan:string
    } 
    const ikun={
    	name:'why',
        age:16,
        slogan:'你干嘛'
        
    }
    
  • TS中接口由类来实现

    interface IKun {
        name: string,
        age: number,
        slogan: string,
        playBasketball: () => void
    }
    interface IRun {
        running: () => void
    }
    class Person implements IKun, IRun {
        name: string
        age: number
        slogan: string
        playBasketball() { }
        running() { }
    }
    const ikun = new Person()
    
  • TS严格字面量赋值检测:

    对于第一次创建的对象字面量,称之为fresh;

    对于新鲜的字面量,会进行严格的类型检测,必须完全满足类型的要求(不能有多余的属性)

  • 存在两条索引签名:

    interface IndexType{
        // 要求一:数字类型对应的返回值类型必须是字符串类型对应的返回值类型的子类型
        [index:number]:string
        [index:string]:any
    
        // 要求二:索引签名中有定义其他属性,该属性对应的的返回值类型必须符合string类型对应的返回值类型
        // aaa:string
        // aaa:boolean
    }
    const names: IndexType = ['aaa', 'b', 'x']
    
    const item1=names[0]
    const fn=names['forEach']
    
  • 抽象类和接口的区别

    抽象类是事物的抽象,抽象类用来捕捉子类的通用特性,接口通常是一些行为的描述

    接口可以被多层实现,而抽象类只能单一继承

    抽象类中可以有实现体,接口中只能有函数的声明

  • ts中的枚举类型

    enum Direction {
        UP, DOWN, RIGHT, LEFT
    }
    const d1: Direction = Direction.UP
    

    当不设置枚举类型的值时默认从0开始一次递加1;也可以为枚举类型设置值:

    enum Direction{
        //给第一个枚举类型设置值为数字100,后续的枚举类型的值依次加1
    	UP:100,DOWN,RIGHT,LEFT
    }
    

    或者为枚举类型设置值为字符串:

    enum Direction{
        //给第一个枚举类型设置值为数字100,后续的枚举类型的值依次加1  
    	UP:'100',DOWN:'1212',RIGHT:'12121',LEFT:'asjakj'
    }
    

——TS泛型编程

  • 泛型就是类型的参数化

    function bar<Type>(arg: Type): Type {
        return arg
    }
    // 1.完整的写法
    const res1 = bar<number>(111)
    const res2 = bar<string>('111')
    // 2.省略的写法
    const res3 = bar('1111')
    let res4 = bar(999)
    
  • 一个函数可以传入多个泛型

    function foo<Type, Element>(arg1: Type, arg2: Element) { }
    foo(1, 2)
    foo<number, number>(10, 20)
    foo<string, { name: string }>('abc', { name: 'why' })
    
  • 泛型在接口中的使用

    interface IKun<Type=string>{//为泛型设置默认值为string
        name:Type
        age:number
        slogan:Type
    }
    const kun:IKun<string>={
        name:'ikun',age:18,alogan:'你干嘛'
    }
    const kun:IKun<number>={
        name:123,age:18,alogan:666
    }
    
  • 泛型在类中的使用

    class Point<Type=number>{
        x:Type
        y:Type
        constructor(x:Type,y:Type){
            this.x=x
            this.y=y
        }
    }
    const p1=new Point(111,222)
    const p2=new Point<string,number>('121',122)
    
  • 泛型的类型约束写法

    interface ILength{
        length:number
    }
    function getInfo<Type extends ILength>(arg:Type):Type{
        return arg
    }
    
  • 在泛型约束中使用类型参数

    //关于keyof:
    interface Ikun{
        name:string,age:number
    }
    type IkunKeys=keyof Ikun//'name'|'age'
    
    
    function getObjectProperty<T,O extends keyof T>(obj:T,key:O){
        return obj[key]
    }
    const info={
        name:'why',age:18,height:1.88
    }
    const name=getObjectProperty(info,'name')
    
  • 映射类型(注意:映射类型不能使用interface来定义

    有时,一个类型需要基于另一个类型,但你又不想拷贝一份,此时可以考虑使用映射类型

    interface Person {
        name: string,
        age: number
    }
    type MapPerson<Type> = {
        [Property in keyof Type]: Type[Property]
    }
    type newPerson=MapPerson<Person>
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIE7x5YI-1680181060253)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230321034412713.png)]

——TS知识扩展

  • 内置类型的导入

    在导入其他模块中定义好的类型时,需要使用type前缀,来表明导入的是一个类型

    import {type IPerson,type IDType} from './utils/type'
    //或者是写成这样:
    import type {IPerson,IDType} from './utils/type'
    const ikun:IPerson={
        name:'string',
        age
    }
    
  • namespace命名空间

    //命名空间、空间内容都需要使用export导出后才可以在其它模块中使用
    export namespace price{
        export function format(){
            return '$'+price
        }
        export const name='why'
    }
    export namespace date{
        export function format(date){
            return date
        }
    }
    

    使用命名空间中的内容

    //导入命名空间
    import {date,price} from './utils/format'
    //使用命名空间中的内容
    price.format('111')
    
  • 关于.d.ts文件:用于类型的声明,称之为类型声明或者类型定义文件,它仅仅用来做类型检测,告知TS我们有哪些类型

  • 外部定义类型声明 – 第三方库

    外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明。

    这些库通常有两种类型声明方式:

    • 方式一:在自己库中进行类型声明(编写.d.ts文件),比如axios

    • 方式二:通过社区的一个公有库DefinitelyTyped存放类型声明文件

      我们安装react的类型声明: npm i @types/react --save-dev

  • 外部定义类型声明 – 自定义声明

    什么情况下需要自己来定义声明文件呢?

    • 情况一:我们使用的第三方库是一个纯的JavaScript库,没有对应的声明文件;比如lodash

      declare module "lodash"{
          export function join(arg:any[]):any
      }
      

      声明模块的语法: declare module '模块名' {}

      在声明模块的内部,我们可以通过 export 导出对应库的类、函数等

    • 情况二:我们给自己的代码中声明一些类型,方便在其他地方直接进行使用;

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7xZUJOt-1680181060256)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230329224012970.png)]

  • declare 声明模块

    我们也可以声明模块,比如lodash模块默认不能使用的情况,可以自己来声明这个模块:

    declare module "lodash"{
        export function join(arg:any[]):any
    }
    

    声明模块的语法: declare module ‘模块名’ {}。

    在声明模块的内部,我们可以通过 export 导出对应库的类、函数等;

  • declare 声明文件

    在某些情况下,我们也可以声明文件:

    比如在开发vue的过程中,默认是不识别我们的.vue文件的,那么我们就需要对其进行文件的声明;

    比如在开发中我们使用了 jpg 这类图片文件,默认typescript也是不支持的,也需要对其进行声明;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-02pijyMJ-1680181060257)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230329234133554.png)]

  • declare 命名空间

    比如我们在index.html中直接引入了jQuery,我们可以进行命名空间的声明:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzegByCw-1680181060258)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230329234709842.png)]

    在main.ts中就可以使用了:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1rgdk5du-1680181060259)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230329234729968.png)]

  • 关于tsconfig.json

    tsconfig.json文件有两个作用:

    • 作用一(主要的作用):让TypeScript Compiler在编译的时候,知道如何去编译TypeScript代码和进行类型检测

      ​ ✓ 比如是否允许不明确的this选项,是否允许隐式的any类型

      ​ ✓ 将TypeScript代码编译成什么版本的JavaScript代码

    • 作用二:让编辑器(比如VSCode)可以按照正确的方式识别TypeScript代码;

      ​ ✓ 对于哪些语法进行提示、类型错误检测等等;

    tsconfig.json在编译时如何被使用呢?

    • 在调用 tsc 命令并且没有其它输入文件参数时,编译器将由当前目录开始向父级目录寻找包含 tsconfig 文件的目录
    • 调用 tsc 命令并且没有其他输入文件参数,可以使用 --project (或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的 目录;
    • 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略

    webpack中使用ts-loader进行打包时,也会自动读取tsconfig文件,根据配置编译TypeScript代码。

    tsconfig.json顶层选项:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S1HMTmPO-1680181060260)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230330002017480.png)]

——TS条件类型

  • 条件类型的基本使用

    很多时候,日常开发中我们需要基于输入的值来决定输出的值,同样我们也需要基于输入的值的类型来决定输出的值的类型

    条件类型(Conditional types)就是用来帮助我们描述输入类型和输出类型之间的关系

    条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression )

    function sum<T extends number|string>(num1:T,num2:T):T extends number? number:string
    function sum(num1,num2){
        return num1+num2
    }
    
  • 在条件类型中推断(infer)

    条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果

    比如我们现在有一个数组类型,想要获取到一个函数的参数类型和返回值类型:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0OtmyO1-1680181060262)(C:\Users\酒忆\AppData\Roaming\Typora\typora-user-images\image-20230330013137977.png)]

  • 分发条件类型

    在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的(distributive)

    通俗讲,就是将联合类型中的每一项挨个去传入条件类型表达式得到结果,再将结果联合

    type ToArray<T>=T extends any?  T[]:never
    
    type NewType =toArray<number|string>
    //得到的结果一定是number[]|string[]而不是(number|string)[]
    

    未完待续。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值