TS

262 阅读9分钟

TypeScript

什么是 ts

​ + 官方: 是一个 javascript 的超集

​ + 私人

​ => 就是一个相对强类型限制的 javascript

​ => 仅仅是为了在开发过程中避免错误而使用的

​ => 在开发过程中避免了因为类型错误而出现的代码运行中不必要的错误

一、编译ts文件

    1. 单文件编译
        + 打开命令行, 目录切换到你要编译的 ts 文件所在的目录
        + 输入指令: $ tsc 你要编译的ts文件名
        + 会生成一个同名的 js 文件

    2. 单文件实时编译
        + 打开命令行, 目录切换到你要编译的 ts 文件所在的目录
        + 输入指令: $ tsc 你要编译的ts文件名 --watch
        + 会生成一个同名的 js 文件

    3. 工程化编译
        + 可以开启一次性编译或者实时编译
        + 目的: 是为了直接编译当前项目的所有 ts 文件
        + 步骤1:
          => 需要你有一个项目根目录(以 "02_编译ts文件/02-3_工程化编译/" 为根目录)
          => 当前项目的所有内容都需要放在当前项目根目录内
        + 步骤2:
          => 创建工程化编译的配置文件
          => 起名叫做 tsconfig.json
          => 该文件需要存放在你的项目根目录
        + 步骤3:
          => 书写配置文件
          => 书写工程化配置项目(详见 03_ts配置文件详解.html)
        + 步骤4:
          => 开始执行
          => 情况1:
            + 根目录
              - tsconfig.json
              + ts/
              + js/
            + 可以直接执行指令
              -> 一次编译: $ tsc
              -> 实时编译: $ tsc --watch
          => 情况2:
            + 根目录
              + sdy
                - tsconfig.json
                + ts/
                + js/
              + xhl
                - tsconfig.json
                + ts/
                + js/
            + 你是在根目录执行指令
            + 你可以直接执行指令
              -> 一次编译: $ tsc --project 项目文件名
              -> 实时编译: $ tsc --project 项目文件名 --watch
        + 当你开启了实时编译以后, 修改 tsconfig.json 配置文件以后
          => 也不需要重启, 会自动检测到 tsconfig.json 文件修改了, 直接重新执行读取

二、ts配置文件

    ts 配置文件详解
        + 在 tsconfig.json 文件内进行 ts 工程化编译配置

    书写
        {
          include: [],
          exclude: [],
          extends: '',
          files: [],
          compilerOptions: { ... }
        }

        include: 表示你要编译哪些文件
           => 你可以直接在数组内书写所有要编译的目录结构
        exclude: 表示你不需要编译哪些文件
           => 把你不需要编译的文件目录全部书写在这里
        extends: 表示继承
           => 该配置文件可以继承一个别的 tsconfig 配置文件
        files: 表示你要编译哪些文件
           => 注意: 一般是在 ts 文件比较少的时候使用, 把你需要编译的 ts 文件一个一个书写在里面
           => 一般不用, 一般直接使用 include 就可以了
           => 用了 include 就不要使用 files
        compilerOptions: 表示 ts编译器 的所有配置选项
           => 和 ts 限制相关的所有选项(详见解释1)

   解释1
      compilerOptions 配置项详解
        target: 'es6', // 编译完毕的 js 文件为哪一个 ES 语法版本 ES3, ES5, ES6, ES2015, ES2016, ...
        module: 'ES6', // 表示代码开发过程中使用的模块化语法规范(详见解释2)
        lib: [], // 指定项目中要用到的库, 一般可选值是 DOMES 的各个版本(详见解释3)
        outDir: '', // 表示编译完毕的文件存放目录
        outFile: '', // 表示生成的文件名(详见解释4)
        rootDir: './', // 表示项目根目录
        strict: 布尔值, // 默认是 false, 超级严格模式配置项, 当他开启为 true 以后, 配置项内所有的格式类型限制都默认为true
        allowJs: 布尔值, // 是否对 js 代码进行编译
        checkJs: 布尔值, // 是否检查js是否符合语法规范
        removeCommons: 布尔值, // 是否移除注释
        noEmit: 布尔值, // 不编译 js 文件
        noEmitOnError: 布尔值, // 当有错误的时候不编译 js 文件了
        alwaysStrict: 布尔值, // 表示编译出来的 js 文件是否采取严格模式(详见解释5)
        noImplicitAny: 布尔值, // 不允许使用隐式 Any 类型
        noImplicitThis: 布尔值, // 不允许使用指向性不明确的 this
		declaration: 布尔值,  是否生成声明文件
        declarationDir: "./dec/",  声明文件的路径
        declarationMap: 布尔值,  生成声明文件的sourceMap
        sourceMap: 布尔值,	生成目标文件的sourecMap
        noFallthroughCasesInSwitch: 布尔值  防止switch语句贯穿
   解释2
      解释 module 配置项
        + ES6 表示你的 ts 代码内使用 ES6 模块化语法规范
          => 导入 import
          => 导出 export
        + CommonJS 表示你的 ts 代码内使用 CommonJS 模块化语法规范
          => 导入 require()
          => 导出 module.exports = {}

   解释3
      解释 lib 配置项, 表示可以在 ts 内使用的内容
      DOM: 所有 ES3ES5 语法规范的所有语法
      ES6: ES6 语法
      ES2019: ES2019 的所有语法和新增API

   解释4
      解释 outFile 配置项
        + 一般不需要书写
        + 因为他只能书写一个名字, 一般表示把所有的 ts 文件合并成为一个 js 文件
        + 你书写在全局的所有变量会进行合并
        + 一般不会使用
 
   解释5
      解释 js 严格模式
        + js 在作用域的开头可以书写一个 'use strict' 表示该作用域代码进入严格模式
        + 要求 js 书写代码必须严谨
        + 不允许定义重名形参
          function fn(a, a) {}
        + 不允许全局使用 this
          => 默认全局 this 是 window
          => 严格模式全局 this 是 undefined
        + 八进制表示
          => 非严格模式下 071
          => 严格模式下 0o71
        + 诸多要求

三、ts语法

​ 在 ts 代码规范内容

​ 推荐定义变量使用 let 关键字

​ 如果你需要定义常量(const) 变量名尽量大写

3.1 基础限制

3.1.1 基础类型限制
  基础类型
  对于基础数据类型的限制
  语法: let 变量名: 类型限制 = 值
  数字
      定义一个叫做 n 的变量, 限制类型为 number 类型
      今后这个变量只能存储 number 类型的数据, 如果存储了其他类型的数据
      那么编辑器会提示错误
      注意: 不是 js 代码语法层面的错误, 而是 ts 的语法规范, 在编辑器提示错误
      let n: number = 100
      n = 200

  布尔
      let b: boolean = true
      b = false

  字符串
      let s: string = 'hello world'
      s = '你好 世界'

  正则
      复杂数据类型的类型限制是首字母大写
      基本数据类型的类型限制是首字母小写
      let reg: RegExp = /^abcd$/

  时间
      let time: Date = new Date()

  函数
      let fn: Function = () => {}
3.1.2 数组限制
  因为数组内要存储数据
  在开发过程中, 一般数组存储的数据类型是一致的
  语法1: let 变量名: 类型[] = 数组
  	限制了 list1 这个数组只能存储 number 类型数据
  		let list1: number[] = [ 100, 200 ]
  		list1.push(300)
        list1.push(300.256)
        list1.push('hello') // 错误

  语法2: let 变量名: Array<类型> = 数组
      let list1: Array<boolean> = [ true, false, true, false ]
      list1.push(100) // 类型错误

  语法3: let 变量名: Array<类型1|类型2> = []
      这个 list1 数组内只可以存储 string 类型 或者 number 类型
      let list1: Array<number|string> = [ 100, 'hello', 'world', 200 ]
      list1.push(true)
      list1.push(/^$/)
3.1.3 元组限制

元组

​ 元组其实也是数组的一种

​ 在开发过程中一个限定了数组长度和类型的数组

​ 并且还限制长度限制内的索引位置

 语法: let 变量: [ 类型1, 类型2 ] = 数组
     限定
        person 变量存储的数据必须是一个数组
        [0] 位置必须是一个 string 类型数据
        [1] 位置必须是一个 number 类型数据
  let person: [ string, number ] = [ 'Jack', 18 ]
  你可以修改已知限制长度的位置, 但是只能修改数据, 不能更换数据类型
      person[0] = 100 // 提示错误
      person[1] = 'Rose' // 提示错误

  元组越界(不推荐元组越界)
      因为元组其实本身也是一个数组, 所以可以存储超出长度限制的内容
      越界以后的数据, 是你限制的数据类型中的任意一个都行
      目前: person 数组从 [2] 开始可以存储 string 或者 number 类型的数据

3.2 特殊限制

  1. any: 表示任意类型, 什么类型都行
      尽量不要使用
      let n: any = 100
      n = 'hello'
      n = true
      n = []
      n = () => {}

  2. void: 表示没有任何类型, 一般用于定义函数返回值
      表示该函数没有返回值, 或者返回的是 undefined 或者 null
      function fn(): void {
        return
      }

  3. undefinednull
      undefined 类型叫做 undefined
      null 类型叫做 null
      一般没有意义
      注意: undefinednull 是所有类型的子类型
          你设置的任何一个类型都可以赋值为 undefinednull
          let u: undefined = undefined
          let n: null = null
          let s: string = null

  4. never
      是一个永不存在的数据类型
      一般也是定义函数的返回值使用
      注意: 表示该函数不能有返回值, 并且该函数必须要意外打断(抛出异常, 死循环)
  
  5. 在 ts 内, Object 类型是一个及其特殊的类型限制
      表示所有基本数据类型以外的所有内容
      当他表示对象的时候表示的是空对象
      注意: 虽然可以被赋值为其他数据类型, 但是不能用其他数据类型的方法
      
      let o: Object = {}
      o = []
      o = /^$/
          你不能用其他数据类型的方法
          o.push(100)
          o.test('abcd')

      注意: Object 限制的对象是一个没有任何成员的对象
      let o: Object = {}
      不能添加其他成员
      o.name = 'Jack'

3.3 断言

​ as 断言

​ +告诉编辑器, 我知道自己在干什么, 你别管我了

​ => 因为当你类型出现问题的时候, 编辑器会提示错误

示例1:
      限制了 n 的类型是 any
      let n: any

      声明 str 的时候, 没有给 str 进行类型限制
      str 就会被推断为 n 的类型
      let str = n

      接下来你可以给 str 赋值为任意数据类型
      str = ''
      str = 100
      str = true

示例2
      限制了 n 的类型是 any
      let n: any = 100

      声明 str 的时候, 没有给 str 进行类型限制
      把 n 赋值给 str, 对 n 做断言限制
      
 语法: 值 as 类型
      语法: <类型>值
      把一个断定为 string 类型的数据赋值给了 str
      str 就会隐式推断出一个 string 类型
      let str = n as string
      let str = <string>n

      接下来你可以给 str 只能赋值为 string 类型了
      str = ''
      str = 100
      str = true

  /*
    const 断言
      + 相当于是一个只读属性的意思
  */

  示例1:
      let o = { name: 'Jack' }
      对象内本身存在的成员是可以做出修改的
      o.name = 'Rose'

  示例2:
      可以在定义的时候, 直接加上 const 断言
      限制了 o 对象内的成员都是只读
      let o = { name: 'Rose' } as const
      当你想修改一个 const 断言内的数据的时候, 会提示错误
      o.name = 'Jack'

3.4 接口

3.4.1 初始接口
    初识接口
      + 对一个数据结构(对象/数组/函数/类) 的抽象描述

    语法:
      interface 接口名称 {
        所有的接口限制
      }
  	接口建议首字母大写
  	
  定义了一个叫做 Person 的接口, 你可以把它当做一个 Person 数据类型
  私人: 相当于一个自定义的数据类型
      interface Person {
        // 该数据类型内需要有一个 name 属性, 值必须是 string 类型
        name: string
        age: number
        gender: string
      }

  对象接口使用
      限制 o1 这个变量保存的必须是符合 Person 接口限制的数据
      注意: 属性多了不行, 少了不行, 值的类型错了不行
      let o1: Person = { name: 'Jack', age: 18, gender: '男' }

  对象接口断言使用
      需求: 一开始是一个空对象, 后期动态添加
      把 {} 赋值给了 obj
        因为 obj 被限制了是 Person 接口类型
        {} 不是一个符合 Person 接口类型的数据
        所以 {} 不能被赋值给 obj
      let obj: Person = {}

  把 {} 赋值给了 obj
    因为 obj 被限制了是 Person 接口类型
    {} 不是一个符合 Person 接口类型的数据
    但是我给 {} 断定为 Person 类型的数据
    所以这里就可以赋值给 obj 了
      let obj: Person = {} as Person
      obj.name = 'Jack'
      obj.age = 18
      obj.gender = '男'
      obj.score = 30 // 不行的
3.4.2 对象接口
  对象接口
      + 利用这个接口来限制对象内的成员
      + 可以限制属性, 也可以限制方法
        => 限制属性   属性名: 类型
        => 限制方法   方法名(): 返回值类型
        
   可选属性
      + 书写可选属性    属性名?: 类型
      
   只读属性
      + 书写只读属性   readonly 属性名: 类型限制   
     
   额外属性
      + 表示对象内我不确定将来会不会增加新的属性
      + 也不确定增加多少个新的属性
      + 书写额外属性    [propname: string]: 类型
      + 注意: 如果你给了值的类型限制, 表示该 接口类型内 就不能存在其他类型的限制
        => 一般我们对于额外属性都会设置成 any 类型
        
  interface Person {
      name: string
      age: number
      desc?: string
      readonly gender: string
      // 需求: 我需要添加额外属性
      // 限制属性名必须是一个字符串类型(问题: 你写哪一个属性名)
      [propname: string]: 值的类型限制(注意: 值的类型限制一般写 any)
   }
3.4.3 函数接口
函数参数
  函数的参数
  给函数定义形参
  function fn(a: number, b: number): number {
    return a + b
  }

  函数可以定义可选参数
  注意: 因为你的参数可选, 一般所以需要在 函数内进行条件判断处理逻辑
  function fn(a: number | string, b?: number): number {
    if (typeof a === 'string') {
      return 0
    }
    if (b) {
      return a + b
    }
    return a * 10
  }
  let a = fn(10)

  函数参数默认值
  如果传递了 a 就使用你传递的
  如果没有传递 a 那么就是使用 10
  function fn(a: number = 10) {}

  多个参数
  function fn(a: string, ...b: Array<any>) {}
  fn('Jack', 10, '20', '30', 40, true)


函数接口
需要限制类型的位置有两个, 一个是参数位置, 还有一个是返回值位置
  
  语法: function fn(参数1: 类型限制, 参数2: 类型限制): 返回值限制 {}
      function fn(a: number, b: number): number {
        return a + b
      }
      
  注意: 函数类型接口是给赋值式函数使用的, 函数表达式使用的, 不是给声明式函数使用的
  let fn: 函数接口 = function () {}

  函数接口
  RandomFunc 就是对于一个函数的限制
       限制函数的参数
       限制函数的返回值
  interface RandomFunc {
    // 要求将来 RandomFunc 接口限制的函数
    //   需要一个 a 参数, 是 number 类型
    //   需要一个 b 参数, 是 number 类型
    //   返回值是 number 类型
    (a: number, b: number): number
  }

  // 利用接口定义函数
  let fn: RandomFunc = (a, b) => a + b

  // 使用函数的时候
  fn(10, 20)
3.4.4 类接口

​ 类的基本书写

​ 不能直接在 constructor 内书写 this.xxx = xxx

​ 必须要提前在 类 里面声明, 我可以有这个属性, 才能书写

   ts 类里面的属性分成五种
      1. 共用属性: 公开的开放的, 就是原先的实例属性
        => 属性名: 类型限制
        => public 属性名: 类型限制
        这个属性实例可以调用, 子类可以继承
   		 	public age: number = 18
   		 	
      2. 静态属性: 给类自己使用的
        => static 属性名: 类型限制
        这个属性由类自己使用, Person.gender
   			static gender: string = '男'
   			
      3. 保护属性: 只能在类和子类中使用, 类内部使用, 实例都不能使用
        => protected 属性名: 类型限制
        将来这个 score 属性只能在类以及子类里面的方法中使用, 实例不可以调用
    		protected score: number = 100
    		
      4. 私有属性: 只能当前类的实例使用, 不能继承
        => private 属性名: 类型限制
        将来这个 classRoom 属性只能当前类里面使用, 不可被继承, 实例不可以使用
   			 private classRoom: string = 'GP30'
   			 
      5. 只读属性: 只能读取值, 不能修改值
        => readonly 属性名: 类型限制
		将来 height 属性只能读取值, 不能修改值
    		readonly height: number = 180
  当你需要真正限制类的时候, 需要两个接口
    一个接口限制实例
    一个接口限制构造器
    
  // 定义类的 实例内容 接口
  interface PeopleClass {
    name: string
    age: number
    sayHi(): void
  }

  // 定义类的 构造器 接口
  interface PeopleConstructor {
    // 要求构造器必须要满足
      // 接受两个参数, 一个 name 一个 age
      // 将来返回的实例满足 PeopleClass 接口要求
    new (name: string, age: number): PeopleClass
  }

  // 注意: 我们没有办法去用函数接口来限制 类
  // 要向真实的限制类, 必须要借用一个工厂函数
    	// 作用: 得到一个满足要求的实例, 满足 PeopleClass 接口要求的实例
  // 将来你想是调用 createPerson 这个函数, 能且只能传递三个参数
        // 第一个是一个构造函数, 必须要满足 PeopleConstructor 接口类型限制
        // 第二个是一个 string 类型数据
        // 第三个是一个 number 类型数据
        // 返回值必须是一个满足 PeopleClass 接口类型限制的对象
  function createPerson(ctro: PeopleConstructor, name: string, age: number): PeopleClass  {
    ctro 形参的限制是由 PeopleConstructor 接口类型限制来的
      // 要求 ctro 的调用必须要和 new 关键字连用
      // 要求 ctro 函数能且只能接受两个参数 name 必须是 string 类型, age 必须是 number 类型
      // 要求 ctro 函数必须要有一个返回值满足 PeopleClass 接口的限制
           //  ctro 得是一个构造函数
            // ctro 返回的实例对象需要包含 { name, age, sayHi() }
    return new ctro(name, age)
  }
例如:
	  // 准备实例接口(主要用来限制对象数据类型)
      // PeopleClass => { name, age, sayHi () {} }
      interface PeopleClass {
        name: string
        age: number
        sayHi(): void
      }

      // 准备类接口(用来限制函数的)
      // PeopleConstructor => function (name, age) { return { name, age, sayHi () {} } }
      // 并且要求函数必须和 new 关键字连用
      interface PeopleConstructor {
        // 有 new 就必须要有是 类, 因为 类才必须和 new 关键字连用
        new (name: string, age: number): PeopleClass
      }

      // 书写工厂函数
      function createPerson(ctro: PeopleConstructor, name: string, age: number): PeopleClass {
        return new ctro(name, age)
      }

      // 定义构造函数
      class Person implements PeopleClass {
        name: string
        age: number

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

        sayHi ():void {}
      }

      // 使用的时候
      let p1 = createPerson(Person, 'Jack', 18)
      console.log(p1)
3.4.5 索引接口
  数组式索引接口, 定义数组(一般比较少用)
  interface PriceList {
    索引必须是数字, 值也是数字
    [index: number]: number
  }

  等价于 let prices: Array<number> = [ 100, 200, 300 ]
  let prices: PriceList = [ 100, 200, 300 ]

  // 对象式索引接口, 定义对象
  interface Person {
    [index: string]: any
  }

  let obj: Person = {}
  obj.name = 'Jack'
  obj.age = 18
3.4.6 继承接口
  // 总接口
  interface People {
    name: string
    age: number
    gender: string
  }

  interface Person {
    say(): void
    play(): void
  }

  // 学生接口 继承自 总接口
  // 可以继承多个接口, 用 逗号(,) 分隔即可
  interface Students extends People, Person {
    score: number
    classRoom: string
  }
3.4.7 泛型接口
  需求: 不确定是什么数据类型, 但是要求 输入 和 输出 是一个类型
  可以使用泛型来定义接口 <T>

  函数泛型接口
  给接口添加泛型
  注意: 就是使用 T 做一个标识符, 表示一个 类型 内容
  两个位置写的都是 T, 表示两个位置的类型是一样的
  interface Dog<T> {
    <T>(name: T): T
  }

  需求: 传递的参数是什么数据类型, 那么返回值也得是什么数据类型(数据类型和值必须一致)
  如果参数是一个数字, 那么返回值也得是一个数字
  如果参数是一个字符串, 那么返回值也得是一个字符串

  这个函数只能接受 string(欠着)
  let fn1: Dog<string> = function <T>(name: T): T {
    return name
  }

  fn1('name')

  // 对象泛型接口
  interface Person<T> {
    [propname: string]: T
  }

  // 使用对象的时候进行限制
  let o1: Person<string> = {
    name: 'Jack',
    age: '18'
  }

  let o2: Person<number> = {
    name: 100,
    age: 18
  }

3.5 约束

3.5.1 基本约束
  直接使用 type 关键字定义约束内容
  这个 Colors 也可以当做一个类型限制来使用
  type Colors = 'red' | 'green' | 'blue'

  使用 Colors 约束的时候
  let bgColor: Colors = 'blue'
  bgColor = 'green'
3.5.2 对象约束
  type Keys = 'name' | 'age' | 'gender'
  // 步骤2: 书写 对象约束(用的比较少)
  type Person = {
    // 属性名: 必须是 Keys 里面约束的几个
    [key in Keys]: string
  }

  // 等价于
  interface Person2 {
    name: string
    age: string
    gender: string
  }

3.6 枚举

3.6.1 基础枚举
 设置 枚举 的时候
   可以不赋值
   默认第一个枚举常量的值是 0
   按照书写顺序开始, 依次递增 1
       enum Week {
         AA,
         BB,
         CC
       }
       
    将来使用的时候
    可以当做类型, 也可以直接使用
    console.log(Week.AA) // 得到的就是 0
    console.log(Week.BB) // 得到的就是 1
    console.log(Week.CC) // 得到的就是 2
    
    两个枚举内的内容是不相等的
3.6.2 数字枚举
  指的是 枚举 内的常量存储的值是 数字
  第一个静态常量默认是 0, 后续的依次 +1
  
  你可以修改任何一个静态常量的值
  从你修改过的哪一个开始, 后续依次 +1
      enum Week {
        AA,      // 0
        BB = 22, // 22
        CC       // 23
      }
      
   也可以修改为浮点数, 后续的依旧是依次 +1
      enum Week {
        AA,      // 0
        BB = 22.3, // 22.3
        CC,      // 23.3
        CC2,     // 24.3
        DD = 33, // 33
        EE       // 34
      }
      
  数字枚举的使用
  可以把 枚举集合 当做一个 类型来使用
  可以把 枚举集合 当做一个 对象来使用
  n 只能使用 Week 集合内的枚举内容
    因为 typescript 在解析数值类型枚举的时候, 因为是按照索引解析的
    所以, 只要是数字常量都可以, 只要不是其他枚举内的内容就可以
3.6.3 字符串枚举
  字符串枚举
  表示值是一个字符串, 不会依次递增
  如果你写了某一个静态常量是字符串, 那么他后面的必须紧跟着赋值
  enum Colors {
    RED = 'red',
    GREEN = 'green',
    BLUE = 'skyblue',
    ORANGE = 'orange'
  }

  color 变量被限制为了 Colors 枚举类型
  因为是字符串枚举, 所以只能使用 Colors 内的枚举内容
3.6.4 常量枚举
  常量枚举
  看编译结果
  普通枚举
    因为会把 WeekA 编译成一个对象
      enum WeekA {
        AA,
        BB
      }

  在赋值的时候, 其实就是把 WeekA 内的 AA 成员赋值给了 n
  在编译的时候, 还是会保留表达式
  let n = WeekA.AA

  常量枚举
    编译的过程中, 因为是一个 const 定义的常量
    永远不可能出现变化
      const enum WeekB {
        AA,
        BB
      }

  在赋值的时候, 会把 WeekB.AA 直接编译出来
  把真实的值赋值给 n2 变量
  let n2 = WeekB.AA

3.7 抽象类

  类的书写 - 抽象类
     定义一个抽象类, 不是为了创造实例对象的, 不能直接 new 来创建实例对象
     专门为了给 子类 继承使用的

  定义一个动物类, 但是是一个抽象类
     这个 Animal 不能直接 new
     将来谁继承我, 谁必须要有 name 和 age 属性
     谁必须要有一个 say 方法
  私人: 作为一个专用父类使用
  注意: 抽象类不能有自己的方法, 他只能定义需要哪些方法, 由子类重写
   
  // 第一种方法
      abstract class Animal {
        name: string
        age: number
        abstract say(): void
      }
      
  定义一个 狗类, 继承自 Animal
  class Dog extends Animal {
    say () {
      console.log(`你好 !! 我叫 ${ this.name }, 我今年 ${ this.age } 岁了`)
    }

    constructor (name: string, age: number) {
      super()
      this.name = name
      this.age = age
    }
  }
  
  // 创建实例
  let d1 = new Dog('最强大脑丁世通', 3)
  console.log(d1)
  d1.say()
  
  
  // 抽象类的第二种使用方式
  // 把构造器一起抽象出来
  abstract class Animal {
    name: string
    age: number
    abstract say(): void

    // 在抽象类内书写构造器
    constructor (name: string, age: number) {
      this.name = name
      this.age = age
    }
  }

  // 定义一个 狗类, 继承自 Animal
  class Dog extends Animal {
    // 在子类中并不影响你 this 的使用
    say () {
      console.log(`你好 !! 我叫 ${ this.name }, 我今年 ${ this.age } 岁了`)
    }
  }
  
  // 创建实例
  let d1 = new Dog('最强大脑丁世通', 3)
  console.log(d1)
  d1.say()

四、函数相关

4.1 函数的参数

  直接给参数添加类型限制
  function fn(x: number, y: number): void {}

  函数参数的可选
  function fn(x: number, y?: number): void {}

  函数参数默认值
  function fn(x: number = 100, y?: number): void {}

  函数的多个参数
  function fn(...args: any[]) {}

4.2 函数的this

  函数的 this
      => 注意: 所指的 this 不是 this指向, 而是 this 的类型限制
      => 在 ts 内, 不允许出现没有合法限制的 this 不允许 this 被推断为 any 类型
        -> 如果配置项内 "noImplicitThis": false
          + 那么你的 this 随便怎么玩都不会提示错误
        -> 如果配置项内 "noImplicitThis": true
          + 不允许 this 被推断为 any 类型
          
      class Area {
        w: number
        h: number
        constructor (w: number, h: number) {
          this.w = w
          this.h = h
        }
		
		示例一:
        因为这个 getArea 是一个构造函数内的方法
        this 天然指向该类的实例
        getArea () {
          console.log(this.w * this.h)
        }
        
        示例二:
        因为这个 getArea 是一个构造函数内的方法
        this 天然指向该类的实例
        内部返回的函数是一个函数表达式了, 内部的 this 不在指向当前类的实例了
        就会在内部函数内, 把 this 推断为 any 类型
        getArea () {
          return function () {
            console.log(this.w * this.h)
          }
   		 }
      }
      
      
  假参数 this
      我们在定义函数的参数的时候, 可以在函数的第一个参数位置定义一个假参数, 叫做 this
      在这个函数内, this 不是真实的第一个参数, 而是函数定义定义的 this 的方向
      不是真实的第一个参数, a 才是函数内使用的第一个参数
      假 this 的方式是不可以全局调用的
      注意: 一般出现在对象内的方法内, 或者原型方法内, 或者回调函数内
      
      示例一:
      function fn(this: void, a: number): void {}
      fn(100)
      给原型上的方法一个固定的 this
          class Area {
            w: number
            h: number
            constructor (w: number, h: number) {
              this.w = w
              this.h = h
            }

        因为这个 getArea 是一个构造函数内的方法
        this 天然指向该类的实例, 这个是推断出来的 this
        表示给 getArea 内的 this 确定了一个 this 是 Area
        给 this 主管定义了指向
            getArea (this: Area) {
              console.log(this.w * this.h)
            }
          }

4.3 回调函数中的this

      const button: HTMLElement = document.querySelector('.btn') as HTMLElement

      // hander 其实就是一个回调函数
      button.addEventListener('click', handler)

      // 需要以一个假参数的形式, 给 this 确定一个指向
      function handler(this: HTMLElement): void {
        // 按说这里面的 this 应该是指向事件源
        // 所以我可以使用 button 变量, 就可以使用 this
        const id: string = this.dataset.id
      }

4.4 函数的重载

   利用重载解决问题
      根据我的需求, 如果我传递的是 string 类型参数, 那么返回值一定是 string 类型
      根据我的需求, 如果我传递的是 number 类型参数, 那么返回值一定是 number 类型
      情况1:
      function fn3(x: string): string
      // 情况2:
      function fn3(x: number): number
      // 情况3:
      function fn3(x: string[]): number
      function fn3(x: string | number | string[]): string | number {
        // 需要在函数内进行条件判断
        if (typeof x === 'number') {
          return Number(x.toString().split('').reverse().join(''))
        } else if (typeof x === 'string') {
          return x.split('').reverse().join('')
        } else {
          return Number(x.join(''))
        }
      }

      fn3(100).toFixed(3)
      fn3('hello').substring(0, 1)
      fn3([ '1', '2' ]).toFixed(3)
      
  解释
    fn2 这个函数
      + 你调用的时候, 传递的参数是 number 类型
      + 问题: 根据你函数定义的类型限制, 你能不能确定函数的返回值就是 number 或者就是 string
        => 不管你传递的是什么参数, 你的返回值有可能是 string 也有可能是 number
      + fn2(100).toFixed(3)
        => 利用返回值调用 toFxied() 方法
        => 因为 toFixed() 这个方法是属于数字的方法
        => 如果你的返回值是 number 数据类型, 那么这个没有问题
        => 如果你的返回值是 string 数据类型, 那么这个就报错了
        => 所以 ts 就会提示你, 这个 fn2(100) 数据是不能调用 toFxied()
        => 所有你的返回值只能调用一些 既属于number类型 也属于string类型 的方法
        => 如果你想调用专门属于 number 类型的方法, 比如 toFixed(), 那么就会提示错误, 因为 string 类型上没有这个方法
        => 如果你想调用专门属于 string 类型的方法, 比如 substring(), 那么就会提示错误, 因为 number 类型上没有这个方法
      + 解决:
        => 根据各种需求和情况, 来百分百确定返回值的类型
        => 利用重载语法, 来说实现, 把我们的各种情况列举出来

五、泛型相关

5.1 基本泛型

    基本泛型
      + 可以在定义函数或者定义接口或者定义类的时候, 不预先执行具体的类型
      + 而是以一个标识符的形式来填充类型
      + **在使用的时候, 再确定类型**
      
      定义函数泛型
      function fn(length: number, value: string): string[] {
        let result = []
        for (let i = 0; i < length; i++) {
          result[i] = value
        }
        return result
      }
      // 向数组内填充 3个 'a'
      let list = fn(3, 'a')
      // 向数组内填充 3个 100
      let list2 = fn(3, '100')

      定义函数的时候, 不使用具体数据类型, 而是以一个标识符代替
      在函数名后面书写标识符
      今后你在该函数参数类型限制位置, 返回值类型限制位置, 函数内的数据类型限制位置
      都可以是直接使用 T 类型
          function fn<T, U>(length: number, value: T, key: U): T[] {
            let result: T[] = []
            for (let i = 0; i < length; i++) {
              result.push(value)
            }
            return result
          }
      // 将来调用 fn 函数的时候, 确定 T 到底是一个什么数据类型
      // 本次调用的时候, 把 T 确定为了 string 类型
      let list1 = fn<string, number>(3, 'a', 100)
      // 本次调用的时候, 把 T 确定为了 number 类型
      let list2 = fn<number, number>(3, 100, 200)

5.2 接口泛型

5.2.1 对象接口
      定义 Person 接口的时候, 定义了两个泛型类型限制
      interface Person<T, U> {
        name: T,
        age: U
      }

      将来在使用接口的时候, 来填充 T 和 U 的内容
      let user: Person<string, number> = {
        name: 'Jack',
        age: 18
      }
5.2.2 函数接口
      interface Func<T> {
        (x: T): T
      }

      使用函数接口
      给函数接口的 T 确定为 string 类型
      let fn: Func<string> = function (x) {
        return x + ''
      }

5.3 类泛型

      class Person<T> {
        w: T
        h: T
        getArea: (w: T, h: T) => T
      }

      // 在实例化的过程中, 给 Person 类内的 T 确定为 number 类型
      let p1 = new Person<number>()
      console.log(p1)
      p1.getArea(100, 200)

六、类型限制高级

6.1 多类型限制

多类型限制

​ 给一个变量限制多个类型

​ 注意: 不推荐这个书写方式

   给 n 确定为 number 数据类型
      let n: number = 100

   给 n2 确定为 number 或者 string 类型
      let n2: number | string = 100
      n2 = 200
      n2 = 'hello'

类型推断

​ ts 会根据你的赋值来进行推断, 确定变量是一个什么类型数据

   	  因为你给 n 赋值为 100, 所以 ts 推断出来 n 是 number 类型
          let n = 100
          let n2: number | string
      不能调用 n2.length
      因为不确定你是 number 还是 string
          console.log(n2.length)
      当你给 n2 赋值以后, 就会进行推断, 推断是什么数据类型, 就按照什么数据类型的规则来解析
          n2 = 'hello'
          console.log(n2.length)
      因为你确实是可以填充 number 或者 string 类型
      所以可以继续更改
          n2 = 100
          n2.toFixed(3)

6.2 symbol

  symbol
      + 明确: symbol 不是 ts 的内容
        => 是一个 js 内在 ES6 阶段新出的数据类型
        => 只不过 ts 内也有 symbol 类型限制
      + 注意: symbol 是一个基本数据类型

  什么是 symbol
      + 表示唯一值, 绝不会重复的值
      + 例子:
        => let a = Symbol(1)
        => let b = Symbol(1)
        => console.log(a === b) // => false
      + 一般使用 symbol 来充当 key 使用
      
      interface Person {
        // 限制某一个 key 是 symbol 类型的限制
        [age: symbol]: number
      }

      // a 保存的是一个 独特的 'age'
      let a = Symbol('age')

      let o = {} as Person
      // 你添加的 key 就是准确的 'age'
      // o.age = 18
      // 你将来访问的时候
      // console.log(o.age)

      // 向 o 内添加了一个叫做 独特的'age' 这个 key
      o[a] = 18
      // console.log(o.age)
      // console.log(o['age'])
      console.log(o[a]) // 因为 a 保存的是一个 独特的'age' => o[独特的'age']

七、装饰器相关

​ 本质是一个函数

​ 其实就是在修饰一个 内容(类 / 类的方法 / 类的属性 / 类方法的参数)

​ 装饰器不是 ts 内专有的内容,是一个 js 的内容 ES6 出现的内容

​ 帮你把一些扩展或者修改的内容进行复用

7.1 认识装饰器

和继承有一些相似,个人理解
        class Student1 {
          play () {}
          say () {}
          study () {}
        }
        
        class Person {
          play () {}
          say () {}
        }
        
        class Student1 extends Person {
          study () {}
        }

        装饰器1: 向当前类上扩展一个 study 方法
        class Person {
          play () {}
          say () {}
        }
        应用装饰器1
        class Student1 extends Person {}

7.2 类装饰器

  // 作为一个装饰器函数, 必须接受参数, 至少一个
  // 使用泛型的形式来确定装饰器的修饰行为
  function fn<T extends People>(Target: T) {
    // 将来这个装饰器给哪一个 类 使用, 那么这个 Target 就指的是哪一个 类
    Target.prototype.say = function () { console.log('我是装饰器扩展的 say 方法') }
    Target.prototype.play = function () { console.log('我是装饰器扩展的 play 方法') }
  }

  // 使用装饰器
  // 注意: 当你开始运行代码的时候, 装饰器函数就会直接执行了
  // 此时: 因为本次 fn 装饰器修饰的是 Student 类, 所以本次 fn 内的 Target 就是 Student 这个类
  // 注意: 因为你是类装饰器, 你在装饰器上向原型上添加的 say 方法
  //   这就导致一个问题, 你的类本身的原型上并不存在这个 say 方法
  //   所以你的实例是不能调用的
  //   在书写类的时候, 必须要写上这个方法

  @fn
  class Student {
    play () {
      console.log('student 原型上的 play')
    }

    // 因为你在类的装饰器上, 有这个方法的扩展, 所以导致这里的 say 方法被覆盖了
    say () {}
  }

  const s = new Student()
  // play 方法是本身书写在 Student 原型上的方法
  s.play()
  // say 方法是因为使用了装饰器由装饰器向 Student 的原型上扩展的方法
  s.say()
  // 装饰器工厂
  // 父类
  class Person {
    say () {
      console.log('我说的是 你好 世界')
    }
    play () {
      console.log('我要玩的是 篮球 足球 乒乓球')
    }
  }

  // 书写一个接口
  interface People {
    new (...args: any[]): {}
  }

  // 书写装饰器
  function fn<T extends People>(Target: T) {
    // 书写一个装饰器工厂
    // 要求: 在装饰器内 return 一个新的构造函数, 新的构造函数继承自 Target
    // 此时就会用这个 类去替换掉你本身书写的类
    return class extends Target {
      sleep () {
        console.log('sleeping')
      }

      play () {
        console.log('我是装饰器的 play')
      }
    }
  }

  // 子类需要实现改写的 play 方法
  // 子类需要实现 sleep 方法
  // 子类需要实现 say 方法
  // 不需要任何自己的多余内容
  @fn
  class Student1 extends Person {}

  @fn
  class Student2 extends Person {}

  const s = new Student1() as any
  s.play()
  s.say()
  s.sleep()

7.3 类方法装饰器

​ 书写一个装饰器, 必须接受三个参数

​ 目的: 改写(重新实现) 类里面的方法

​ 该装饰器是修饰类里面方法的

  // 书写装饰器
  // 这个装饰器将来用来修饰类的方法
  // 注意: 如果你使用这个 类方法装饰器, 那么你的 node 版本必须是 16 以上
  //      因为 tsc 是基于 node 环境运行的, 如果是 16 以下的版本是编译不出来 descriptor 的
  function fn(Target: any, name: string, descriptor: TypedPropertyDescriptor<Function>) {
    // Target 接受的就是当前类的实例的原型
    // name 接受的就是你要修饰的函数名
    // descriptor 接受的是一个当前函数的描述对象, 是一个数据劫持形式(defineProperty)
    console.log('target : ', Target)
    console.log('name : ', name)
    console.log('descriptor : ', descriptor)

    // 向描述信息内的 value 赋值, 给一个新的函数
    descriptor.value = function () {
      console.log('我是被装饰器修改后的函数, 我玩的是 代码, 玩的是 jQuery 玩的是 js 玩的是 ts 玩的是 Vue')
    }
  }


  class Student1 {
    @fn
    play () {
      console.log('我玩的是 篮球 足球 羽毛球')
    }

    say () {}
  }

  class Student2 {
    play () {
      console.log('我玩的是 台球 乒乓球 排球')
    }

    say () {}
  }

  const s1 = new Student1()
  const s2 = new Student2()
  s1.play()
  s2.play()
  // 带有参数的装饰器(装饰器工厂)

  function fn(str: string, year: number) {
    return function (Target: any, name: string, descriptor: TypedPropertyDescriptor<Function>) {
      descriptor.value = function () {
        console.log(`我玩的是 ${ str }, 已经玩了 ${ year } 年了`)
      }
    }
  }


  class Student1 {
    // 第一个人玩的 Vue
    @fn('Vue', 10)
    play () {
      console.log('我玩的是 篮球 足球 羽毛球')
    }

    say () {}
  }

  class Student2 {
    // 第二个人玩的是 React
    @fn('React', 8)
    play () {
      console.log('我玩的是 台球 乒乓球 排球')
    }

    say () {}
  }

  const s1 = new Student1()
  const s2 = new Student2()
  s1.play()
  s2.play()


  /*
    class Student1 {

      // 表示使用 fn 函数去修饰 play 方法
      // @ 表示装载 装饰器
      // fn 表示的是一个函数地址
      @fn
      play () {}

      // 表示使用 fn2('Vue') 函数的返回值去修饰 play 方法
      // @ 表示装载 装饰器
      // fn2('Vue') 把函数调用了, 把返回值放在这当做装饰器函数
      @fn2('Vue')
      say () {}

    }
  */

7.4 类属性装饰器

​ 修饰类里面属性的装饰器

​ 作用: 修改该属性的值

  // 书写装饰器
  // 属性装饰器, 用不到第三个参数, 接受两个即可
  // 注意: 属性装饰器, 不能修改该属性的限制数据类型的, 只能修改属性的值
  // 属性装饰器工厂
  function fn(time: Date) {
    return function (Target: any, name: string) {
      console.log('Target : ', Target)
      console.log('name : ', name)
      // 修改该属性的值
      let val = `${ time.getFullYear() }-${ time.getMonth() + 1 }-${ time.getDate() }`

      Object.defineProperty(Target, name, {
        value: val
      })
    }
  }

  class Student1 {
    // 使用 fn 装饰器来修饰这个 name
    @fn(new Date(1652500063190))
    time: string

    constructor () {}
  }

  class Student2 {
    @fn(new Date())
    time: string
  }

  const s = new Student1()
  const s2 = new Student2()
  console.log(s)
  console.log(s2)

7.5 类方法参数装饰器

​ 类方法参数装饰器

​ 注意: 只能做监控参数是否传递了, 没法修改

​ 注意: 必须要使用装饰器工厂形式

​ 不是语法必须的, 而是 ts 在解析参数装饰器的时候, 不是很完善, 没有办法做到不传递参数

  // 书写装饰器
  // 直接书写成装饰器工厂
  // 必须接受三个参数,
  function fn(n: any) {
    console.log('n : ' , n)
    return function (Target: any, name: string, index: number) {
      // Target 还是原型
      // name: 函数名
      // index: 索引位置
      console.log('Target : ', Target)
      console.log('name : ', name)
      console.log('index : ', index)
      // 查看函数实参的个数
      console.log(Target[name].length)
    }
  }

  class Student {
    play (
      // 修饰参数, 就放在参数的前面
      @fn(20)
      n: number
    ) {
      console.log(n)
    }
  }

  const s1 = new Student()
  s1.play(100)

八、命名空间

详细见git地址gitee.com/du-changke/…

认识命名空间

​ 官方: 把一类内容整合在一起, 放在同一个命名空间下, 利于代码维护

​ 私人: 相当于一个 ts 里面的 "模块化" 语法

​ ts 内不仅仅有模块, 还有 命名空间

学习命名空间(使用)

​ 非工程化命名空间(没有 tsconfig.json 文件)

​ 工程化命名空间(有 tsconfig.json 文件)

8.1 非工程化

// 进行整合
// 把所有文件书写在一个 命名空间下
// 其实就是创建一个对象, 存储这些内容(看一下编译结果)
// 语法: namespace 空间名称 {  }
// 注意: 写在 命名空间 大括号内的内容, 别的文件不能用, 需要在命名空间内用 exports 导出
// 注意: 如果你没有使用, 那么不会被编译, 需要使用以后才会被编译
// 需要引入命名空间, 只有引入以后, 编译才能正常编译
// 语法: 三斜线指令(有严格的书写语法要求)
//      三斜线 <reference path="你要引入的路径" />
//      三斜线指令必须要写在文件的开头, 前面只能有注释内容, 不能有代码, 一旦有了代码, 就不是三斜线引入指令
/// <reference path="./interface.ts" />
/// <reference path="./class.ts" />

8.2 工程化配置

    工程化配置
        + 配置 outFile 书写生成的文件名
        + 配置 module: 'AMD' 形式
          => 因为原生浏览器不支持 ES6 模块化语法
        + 当你使用了工程化以后
          => 因为 outFile 只能生成一个文件
          => 会自动把所有当前项目内的每一个内容, 都集成到这个一个文件内
          => 按照文件顺序进行编译
        + 当顺序打乱了以后
          => 我们文件之间互相的依赖关系就会出现问题
          => 所以必须要写入 三斜线指令 来注明依赖关系

8.3 命名空间嵌套

九、声明文件