TypeScript学习总结,并爬取一些表情包做练习(仅用作学习)

152 阅读10分钟

typescript是很久以前学习的,在实际开发中也不经常使用。为了提升自己,现在也需要抓起来啦。下面是自己以前总结的一些笔记,现在在复习一下。

以前简单写过一篇ts的总结文章,这篇文章接着补充一下吧。

typescript类型

  • null undefined和null值才能赋值给null类型的变量。
    let num: null = undefined
    num = null
  • undefined null和undefined值才能赋值给undefined类型的变量。
    let num: undefined = undefined
    num = null

null和undefined是所有类型的值类型。他们可以赋值给任意类型。

  • symbol
  • boolean
  • void 表示该函数没有返回值。我们可以将null和undefined赋值给void类型,也就是函数可以返回null或者undefined
  • number
  • string
  • object 只能将包装类或者对象类型或者null或者undefined的值赋值给object
    let o: object = new String()
    o = undefined
    o = null
    o = [1, 2]
  • any
    • 当不知道该变量的类型时,防止报错,将其赋值为any
    • 当你不知道数组中元素有那些类型的时候
        let list: any[] = [1, true, "free"];
        list[1] = 100;
    
  • enum
  • tuple
  • never 如果一个函数中没有办法将其内容执行完就结束了,他的返回值类型就是never。并且该函数不能有返回值。
  • unknown unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量。 unknown类型只能赋值给any和unknown类型。any类型可以赋值给任意类型。所以unknown相比于any更安全。

在给声明变量的时候不给变量初始值,我们必须加上类型注解,不然变量的类型为any。

函数

两种定义函数类型的方式

  • 在声明函数的时候指定参数类型和返回值类型
    function func (str: string) : number {
      return 0;
    }
  • 定义一个函数的类型变量。
    const func2 : (str: string) => number = () => 0

函数参数可选性

  • 通过在参数后面写上?来表示该参数时可选参数。
  • 可选参数一定在必选参数之后
  • 可选参数不能赋初值
    function fn1(name: string, age?: number = 3) { // 报错 参数不能包含问号和初始化表达式。
      return age
    }

指定参数的默认值

只有传递了undefined(或者不传递值)才会使用默认初始化值


    function foo111(first: string, last: string = 'zh') {
      return first + last
    }

    console.log(foo111('llm', undefined)) //llm zh
    console.log(foo111('llm', null)) //llm null

可选参数和默认值参数的位置关系

  • 可选参数一定要放在必须参数后面,这是typescript的规定
  • 默认值参数可以放在必须参数前面或者后面,但是我们放在前面的时候必须,如果想使用默认值,就必须传入undefined

函数重载

我们不能像java一样,在声明方法的时候来实现重载,只能先定义函数的重载类型。

// 定义函数的重载类型
// 函数的重载: 函数的名称相同, 但是参数不同的几个函数, 就是函数的重载
function add1(num1: number, num2: number): number // 没函数体
function add1(num1: string, num2: string): string // 没函数体
function add1(num1: string, num2: number): number // 没函数体
function add1(num1: number, num2: string): string // 没函数体

// 实现重载函数的内部逻辑
function add1(num1, num2) {
  return num1 + num2
}

只读数组

通过ReadonlyArray来定义。他只是定义了数组中的元素不能修改,但是可以对数组整体修改

    let arr: ReadonlyArray<number> = [1, 2, 3]
    arr[0] = 9; // 报错
    arr = [2, 2, 3] // 不报错

类中区分实例方法和原型方法

通过方法简写的方式定义的方法 -----> 原型方法

  • 这种方式的方法继承后可以被重写,重写后调用的是重写的方法

通过函数表达式的方式定义的方法 -----> 实例方法

  • 通过匿名函数实现方法
  • 通过箭头函数实现方法
    • 这种方式的方法继承后重写是无效的
    class Parent {
      constructor() {
        ////这里的this 已经确定
        this.setup()
      }
    ​
      setup = () => {
        console.log("parent")
      }
    }
    ​
    class Child extends Parent {
      constructor() {
        super()
      }
    ​
      //实例上的方法
      setup = () => {
        console.log("child")
      }
    }
    ​
    const child = new Child() // parentclass Parent2 {
      constructor() {
        this.setup()
      }
    ​
      //原型上的方法
      setup() {
        console.log("parent")
      }
    }
    ​
    class Child2 extends Parent2 {
      constructor() {
        super()
      }
      setup() {
        console.log("child")
      }
    }
    ​
    const child2 = new Child2() // child

类型兼容性

多的可以赋值给少的类型,就是函数参数类型赋值是相反的

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。

    interface Named {
        name: string;
    }

    let x: Named;
    // y's inferred type is { name: string; location: string; }
    let y = { name: 'Alice', location: 'Seattle' };
    x = y;

少参数的函数类型,可以赋值给多参数的函数类型。我们可以类比js函数参数的传递,调用函数的时候传入的参数是可以少于定义的形参的。

    let x = (a: number) => 0;
    let y = (b: number, s: string) => 0;

    y = x; // OK
    x = y; // Error

但是对于函数的返回值而言,依旧是遵循多的赋值给少的。

    let x = () => ({ name: 'Alice' })
    let y = () => ({ name: 'Alice', location: 'Seattle' })

    x = y // OK
    y = x // Error, because x() lacks a location property

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。

    enum Status { Ready, Waiting };
    enum Color { Red, Blue, Green };

    let status = Status.Ready;
    status = Color.Green;  // Error

对于类类型而言

  • 类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。
  • 比较两个类类型的对象时,只有实例的成员(属性或者方法)会被比较。 静态成员和构造函数不在比较的范围内。
    class Animal {
      feet: number
      constructor(name: string, numFeet: number) {}
    }

    class Size {
      feet: number
      static size: number
      constructor(numFeet: number) {}
    }

    let animal1: Animal
    let s: Size

    animal1 = s // OK
    s = animal1 // OK

类的装饰器

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项。

  "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */

执行tsc --init会创建一个tsconfig.json文件,然后将启动上面的配置即可。

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

类的装饰器

  • 可以给类定义多个装饰器,并且执行的过程是从下到上。
  • 如果想要给装饰器传入参数,那么我们可以使用一个函数返回一个和装饰器函数。
  • 装饰器函数接收类的构造函数作为参数。
    function decorator(isFlag: boolean) {
      if (isFlag) {
        return function (constructor: any) {
          constructor.prototype.sayName = function () {
            console.log('llm')
          }
        }
      } else {
        return function (constructor: any) {}
      }
    }

    function decorator1(constructor: any) {
      constructor.prototype.sayName = function () {
        console.log('zh')
      }
    }

    @decorator(true)
    @decorator1
    class Person {}

    const p = new Person()
    ;(p as any).sayName() // llm 

上面这种写法,给类添加了方法后,使用时并没有提示。所以下面给出一个标准的写法。

function testDecorator() {
  return function <T extends new (...args: any[]) => any>(constructor: T) {
    return class extends constructor {
      sayName() {
        return this.name;
      }
    };
  };
}

const Test = testDecorator()(
  class {
    name: string;
    constructor(name: string) {
      this.name = name;
    }
  }
);

const test = new Test('zh');
console.log(test.sayName()); // zh

看见<T extends new (...args: any[]) => any>这个泛型有点奇怪。来解释一下,他表示T类型是一个接受任意多个参数的构造函数。

函数装饰器

普通方法的装饰器

  • 装饰器参数一:表示该方法所属类的原型。constructor.prototype
  • 装饰器参数二:表示该方法的方法名。
  • 装饰器参数三:表示对于该方法的描述。类型为PropertyDescriptor, 类似于Object.defineProperty中的属性描述对象。
    function funcDecorate(
      target: any,
      funcName: string
      descriptor: PropertyDescriptor
    ) {
      console.log(target === Teacher.prototype, funcName) // true sayName
    }

    class Teacher {
      name: string
      constructor(name: string) {
        this.name = name
      }

      @funcDecorate
      sayName() {
        return this.name
      }
    }

    const t = new Teacher('zh')
    t.sayName()

静态方法的装饰器

  • 参数一:表示该方法所属的类的构造函数。constructor
  • 参数二:表示该方法的方法名。
function funcDecorate(
  target: any,
  funcName: string
  descriptor: PropertyDescriptor
) {
  console.log(target === Teacher, funcName) // true sayName
}

class Teacher {
  name: string
  constructor(name: string) {
    this.name = name
  }

  @funcDecorate
  static foo() {}
}

const t = new Teacher('zh')

方法描述符的介绍

  • writable表示方法是否可以被修改。
function funcDecorate(
  target: any,
  funcName: string,
  descriptor: PropertyDescriptor
) {
  descriptor.writable = false // 设置为false,则表示不能修改该方法,即不能重新赋值
}

class Teacher {
  name: string
  constructor(name: string) {
    this.name = name
  }

  @funcDecorate
  sayName() {
    return this.name
  }

  // @funcDecorate
  // static foo() {}
}

const t = new Teacher('zh')
t.sayName = function () {
  // 编译不报错,但是运行报错
  return '111'
}
t.sayName()
  • value表示在修饰器中修改函数。 即使writable设置为false,依旧是可以通过value修改的。
    function funcDecorate(
      target: any,
      funcName: string,
      descriptor: PropertyDescriptor
    ) {
      descriptor.writable = false // 设置为false,则表示不能修改该方法,即不能重新赋值
      descriptor.value = function () {
        return '修改'
      }
    }

访问器装饰器

该装饰器的参数和函数装饰器的参数是一样的。

注意:属性的get 、 set方法只能设置一个修饰器,不弄同时设置相同的装饰器。

    function visitDecorate(
      target: any,
      funcName: string,
      descriptor: PropertyDescriptor
    ) {
      descriptor.writable = false // 设置为false,则表示不能对访问器重新赋值
    }

    class Teacher2 {
      private _name: string
      constructor(name: string) {
        this._name = name
      }

      @visitDecorate
      get name() {
        return this._name
      }

      set name(value) {
        this._name = value
      }
    }

    const t2 = new Teacher2('zh')
    t2.name = 'llm' // 报错

属性装饰器

该装饰器函数只能接收两个参数。

  • 参数一表示该属性对应类的原型对象。
  • 参数二表示该属性的属性名。
    function propDecorate(target: any, propName: string) {
      // 参数一表示该属性类的原型对象
      // 参数二表示该属性的名字
      console.log(target == Teacher3.prototype, propName) // true name
    }

    class Teacher3 {
      @propDecorate
      name: string
      constructor(name: string) {
        this.name = name
      }
    }

    new Teacher3('zh')

我们可以在修饰器函数中返回一个属性描述对象,来替代属性原本的描述对象的。

 + function propDecorate(target: any, propName: string): any {
      // 参数一表示该属性类的原型对象
      // 参数二表示该属性的名字
      console.log(target == Teacher3.prototype, propName) // true name
  +    let descriptor: PropertyDescriptor = {
  +      writable: false
  +    }
  +    return descriptor
    }

    class Teacher3 {
      @propDecorate // 这里的修饰器返回一个属性描述对象,将会改变name属性的描述对象
      name: string
      constructor(name: string) {
        this.name = name
      }
    }

    const t3 = new Teacher3('zh')
  +  t3.name = 'llm' // 这里将会报错

参数装饰器

参数装饰器函数参数

  • 参数一:表示该参数对应方法类的原型对象
  • 参数二:该参数对应的方法名
  • 参数三:该参数在参数列表中的索引
    function paramDecorate(target: any, key: string, paramIndex: number) {
      console.log(target === Teacher3.prototype, key, paramIndex) // true sayText 1
                                                                  // true sayText 0
    }

    class Teacher3 {
      // @propDecorate
      name: string
      constructor(name: string) {
        this.name = name
      }

      sayText(@paramDecorate text: string, @paramDecorate other: any) {
        return text
      }
    }

    const t3 = new Teacher3('zh')

一个小案例

我们可以使用装饰器,来对方法抛出的异常做统一处理。

    const userInfo: any = undefined

    function catchError(msg: string) {
      return function (target: any, key: string, descriptor: PropertyDescriptor) {
        // 获取当前方法
        const fn = descriptor.value
        descriptor.value = function () {
          try {
            fn()
          } catch (e) {
            console.log(msg)
          }
        }
      }
    }

    class Test1 {
      @catchError('userInfo.name 不存在')
      getName() {
        return userInfo.name
      }
      @catchError('userInfo.age 不存在')
      getAge() {
        return userInfo.age
      }
    }

    const test1 = new Test1()
    test1.getName() // userInfo.name 不存在
    test1.getAge() // userInfo.age 不存在

下面我们就通过爬虫实操一下

安装依赖库

    npm install cheerio superagent 
    npm install @types/superagent @types/cheerio -D

Crawler.js

    import superAgent from 'superagent'
    import { resolve } from 'path'
    import MemeAnalysis from './memeAnalysis'
            
    class Crawler {
      // 读取文件输出文件,为了增添内容
      private filePath = resolve(__dirname, '../data/data_json.json')
      constructor(private url: string, private analyzer: MemeAnalysis) {
        this.initSpiderProcess()
      }
      
      // 分析html, 输出内容
      private async initSpiderProcess() {
        // 获取html
        const rawHtml = await this.getRawHtml(this.url)
        // 调用分析器对象,分析html标签,并将分析结果写入文件
        this.analyzer.analyze(rawHtml, this.filePath)
      }
      
      // 发送http请求,获取html文件
      async getRawHtml(url: string) {
        const res = await superAgent.get(url)
        return res.text
      }
    }

    const url = 'https://www.fabiaoqing.com/bqb/detail/id/54836.html'
    const url2 = 'https://www.fabiaoqing.com/bqb/detail/id/54857.html'

    const memeAnalysis = MemeAnalysis.getInstance()
    new Crawler(url, memeAnalysis)
    new Crawler(url2, memeAnalysis)

分析器类 MemeAnalysis.js

    import cheerio from 'cheerio'
    import fs from 'fs'

    interface Items {
      title: string | undefined
      imgUrl: string | undefined
    }

    export default class MemeAnalysis {
      // 单例模式
      private static instance: MemeAnalysis

      private constructor() {}

      static getInstance() {
        if (!MemeAnalysis.instance) {
          MemeAnalysis.instance = new MemeAnalysis()
        }
        return MemeAnalysis.instance
      }

      // 解析html
      private parseHtml(html: string): Items[] {
        const $ = cheerio.load(html)
        const imgs = $('.bqppdiv1')
        
        // 获取爬取的资源,并放进dataJson中
        const dataJson: Items[] = []
        imgs.map((index, item) => {
          const desc = $(item).find('.lazy')
          const title = desc.attr('title')
          const imgUrl = desc.attr('data-original')
          dataJson.push({ title, imgUrl })
        })
        return dataJson
      }

      // 读取原来的data_json文件,保存文件中的值。
      private readFile(filePath: string) {
        let data: Items[] = []
        if (fs.existsSync(filePath)) {
         
          const dataJson = fs.readFileSync(filePath, 'utf-8') || '[]'

          data = JSON.parse(dataJson)
        }
        return data
      }

      // 将爬取的数据写入文件
      private writeFile(newData: Items[], filePath: string) {
        const data = this.readFile(filePath)
        fs.writeFileSync(filePath, JSON.stringify([...data, ...newData]))
      }

      public analyze(html: string, filePath: string) {
        const dataJson = this.parseHtml(html)
        this.writeFile(dataJson, filePath)
      }
    }

爬取的内容,输出到 data_json.json 文件中 image.png 表情包网站

xdm, TypeScript的学习任重而道远,主要还是得把它运用在开发中。但是对于我们写项目而言,就是定义一下类型而已。