TypeScript 知识汇总(三)(3W 字长文)

文章使用的 TypeScript 版本为3.9.x,后续会根据 TypeScript 官方的更新继续添加内容,若是有的地方不同多是版本报错的问题,注意对应版本修改便可。html

前言

该文章是笔者在学习 TypeScript 的笔记总结,期间寻求了许多资源,包括 TypeScript 的官方文档等多方面内容,因为技术缘由,可能有不少总结错误或者不到位的地方,还请诸位及时指正,我会在第一时间做出修改。前端

文章中许多部分的展现顺序并非按照教程顺序,只是对于同一类型的内容进行了分类处理,许多特性可能会提早使用,若是遇到不懂的地方能够先看后面内容。node

下面内容接 TypeScript 知识汇总(二)(3W 字长文)react

8.TypeScript 中的模块

与 ES6 同样,TypeScript 也引入了模块化的概念,TypeScript 也可使用 ES6 中的 export、export default 和 import 导出和引入模块类的数据,从而实现模块化git

ES6 标准与 Common.js 的区别es6

  • require: node 和 es6 都支持的引入
  • export 和 import: ES6 支持的导出引入,在浏览器和 node 中也不支持(node 8.x 版本之后已经支持),须要 babel 转换,并且在 node 中会被转换为 exports,可是在 TypeScipt 中使用编译出来的 JS 代码能够在 node 中运行,由于会被编译为 node 认识的 exports
  • module.exports 和 exports: 只有 node 支持的导出

注: ES6 的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,因此没法实现条件加载github

8.1 导出

8.1.1 导出声明

任何声明(好比变量、函数、类、类型别名或接口)都可以经过添加export关键字来导出typescript

export interface StringValidator {
  isAcceptable(s: string): boolean
}
复制代码
export const numberRegexp = /^[0-9]+$/

export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s)
  }
}
复制代码

8.1.2 导出语句

//上面的语句能够直接经过导出语句来写
const numberRegexp = /^[0-9]+$/
interface StringValidator {
  isAcceptable(s: string): boolean
}
class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s)
  }
}
export { ZipCodeValidator }
export { ZipCodeValidator as mainValidator } //as可以改变导出变量的名字,在外部接收时使用
复制代码

8.1.3 默认导出

每一个模块均可以有一个default导出,默认导出使用 default关键字标记,而且一个模块只可以有一个default导出。须要使用一种特殊的导入形式来导入 default导出。经过export default导出的值能够用任意变量进行接收shell

注:json

  • 类和函数声明能够直接被标记为默认导出,标记为默认导出的类和函数的名字是能够省略的

    //ZipCodeValidator.ts
    export default class ZipCodeValidator {
      static numberRegexp = /^[0-9]+$/
      isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s)
      }
    }
    复制代码
    import validator from './ZipCodeValidator'
    let myValidator = new validator()
    复制代码
  • export default导出也能够是一个值

    //OneTwoThree.ts
    export default '123'
    复制代码
    import num from './OneTwoThree'
    console.log(num) // "123"
    复制代码

8.1.4 导出模块

TypeScript 提供了export =语法,export =语法定义一个模块的导出对象

注意:

  • 这里的对象一词指的是类、接口、命名空间、函数或枚举
  • 若使用export =导出一个模块,则必须使用 TypeScript 的特定语法import module = require("module")来导入此模块
//ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/
class ZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s)
  }
}
export = ZipCodeValidator
复制代码
import zip = require('./ZipCodeValidator')

// Some samples to try
let strings = ['Hello', '98052', '101']

// Validators to use
let validator = new zip()

// Show whether each string passed each validator
strings.forEach((s) => {
  console.log(
    `"${s}" - ${validator.isAcceptable(s) ? 'matches' : 'does not match'}`
  )
})
复制代码

8.2 导入

模块的导入操做与导出同样简单,可使用如下 import形式之一来导入其它模块中的导出内容

import { ZipCodeValidator } from './ZipCodeValidator'
let myValidator = new ZipCodeValidator()
复制代码
//能够对导入内容重命名
import { ZipCodeValidator as ZCV } from './ZipCodeValidator'
let myValidator = new ZCV()
复制代码
//将整个模块导入到一个变量,并经过它来访问模块的导出部分
import * as validator from './ZipCodeValidator'
let myValidator = new validator.ZipCodeValidator()
复制代码
//导入默认模块
//能够对导入内容重命名
import ZCV from './ZipCodeValidator'
let myValidator = new ZCV()
复制代码

固然,也能够直接使用import导入一个不须要进行赋值的模板,该模板会自动进行内部的代码

import './my-module.js'
复制代码

8.2.1 动态导入

import 的导入导出默认是静态的,若是要动态的导入导出可使用 ES6 新增的import()函数实现相似require()动态导入的功能

注:

  • 使用import()函数返回的是 Promise 对象
  • 若是是commonjs格式的模块须要咱们手动调用default()方法得到默认导出
async function getTime(format: string) {
  const momment = await import('moment')
  return moment.default().format(format)
}
// 使用async的函数自己的返回值是一个Promise对象
getTime('L').then((res) => {
  console.log(res)
})
复制代码

8.3 仅限类型导入和导出

import type { SomeThing } from './some-module.js'

export type { SomeThing }
复制代码

该语法为 TypeScript 3.8 新增,像上面这样,只导入或导出某个特定类型,该声明仅用于类型注释,在运行时会被消除。

值得注意的是,类在运行时具备值,在设计时具备类型,而且使用上下文很敏感。使用导入类时,不能执行从该类扩展之类的操做,引入使用了import type后咱们仅把其看成一个类型来使用。

import type { Component } from 'react'

interface ButtonProps {
  // ...
}

class Button extends Component<ButtonProps> {
  // ~~~~~~~~~
  // error! 'Component' only refers to a type, but is being used as a value here.
  // ...
}
复制代码

8.4 export = 和 import = require()

CommonJS 和 AMD 的环境里都有一个exports变量,这个变量包含了一个模块的全部导出内容。CommonJS 和 AMD 的exports均可以被赋值为一个对象, 这种状况下其做用就相似于 es6 语法里的默认导出,即 export default语法了。虽然做用类似,可是 export default 语法并不能兼容 CommonJS 和 AMD 的exports

为了支持 CommonJS 和 AMD 的exports, TypeScript 提供了export =语法。export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举

import module = require("module")也是 TypeScript 新增的一种导入格式,该格式的导入能够兼容全部的导入格式,可是注意若是是引入的 ES6 特有的导出会默认把导出的模块转换为对象(由于 module 只可以接受一个值,默认应该要获取到全部的导出),同时该对象会多一个__esModule值为true的属性(),而其余的全部属性会加载这个对象中

注: 即便使用的是export default在也会是一样的效果,不过会把默认导出添加到一个default属性上

注意:

  • export =在一个模块中只能使用一次,因此是与CommonJS同样基本都是用于导出一个对象出来
  • ES6import ... from ...的默认导出的语法不能做用在export =导出的对象,由于没有default对象,就像CommonJSmodule.exports同样(虽然最后是转换为这个),而ES6export default转换为CommonJS就是为其添加一个default属性
  • 若使用export =导出一个模块,则必须使用 TypeScript 的特定语法import module = require("module")来导入此模块
  • 除了import module = require("module")导入ES6的模块有区别以外,在导入 CommonJS 和 AMD 效果相似,若是在都支持的模块中(UMD 模块为表明),该导入至关因而导入了 ES6 模块中的default
// ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/
class ZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s)
  }
}
export = ZipCodeValidator
复制代码
// Test.ts
import zip = require('./ZipCodeValidator')

// Some samples to try
let strings = ['Hello', '98052', '101']

// Validators to use
let validator = new zip()

// Show whether each string passed each validator
strings.forEach((s) => {
  console.log(
    `"${s}" - ${validator.isAcceptable(s) ? 'matches' : 'does not match'}`
  )
})
复制代码

8.4.1 生成模块代码

在以前说到的import module = require("module")的区别的缘由是根据编译时指定的模块目标参数,编译器会生成相应的供 Node.js (CommonJS),Require.js (AMD),UMDSystemJSECMAScript 2015 native modules (ES6)模块加载系统使用的代码。如:

  • SimpleModule.ts

    import m = require('mod')
    export let t = m.something + 1
    复制代码
  • AMD / RequireJS SimpleModule.js

    define(['require', 'exports', './mod'], function (require, exports, mod_1) {
      exports.t = mod_1.something + 1
    })
    复制代码
  • CommonJS / Node SimpleModule.js

    let mod_1 = require('./mod')
    exports.t = mod_1.something + 1
    复制代码
  • UMD SimpleModule.js

    ;(function (factory) {
      if (typeof module === 'object' && typeof module.exports === 'object') {
        let v = factory(require, exports)
        if (v !== undefined) module.exports = v
      } else if (typeof define === 'function' && define.amd) {
        define(['require', 'exports', './mod'], factory)
      }
    })(function (require, exports) {
      let mod_1 = require('./mod')
      exports.t = mod_1.something + 1
    })
    复制代码
  • System SimpleModule.js

    System.register(['./mod'], function (exports_1) {
      let mod_1
      let t
      return {
        setters: [
          function (mod_1_1) {
            mod_1 = mod_1_1
          }
        ],
        execute: function () {
          exports_1('t', (t = mod_1.something + 1))
        }
      }
    })
    复制代码
  • Native ECMAScript 2015 modules SimpleModule.js

    import { something } from './mod'
    export let t = something + 1
    复制代码

8.4.2 可选的模块加载

有时候,你只想在某种条件下才加载某个模块。 在 TypeScript 里,使用下面的方式来实现它和其它的高级加载场景,咱们能够直接调用模块加载器而且能够保证类型彻底。

编译器会检测是否每一个模块都会在生成的 JavaScript 中用到。 若是一个模块标识符只在类型注解部分使用,而且彻底没有在表达式中使用时,就不会生成 require这个模块的代码。略掉没有用到的引用对性能提高是颇有益的,并同时提供了选择性加载模块的能力

import a = require('./a') // 若是只写这句话是不会引入a模块的
console.log(a) // 必需要使用过才会真正引入
复制代码

这种模式的核心是import id = require("...")语句可让咱们访问模块导出的类型。 模块加载器会被动态调用(经过 require),就像下面if代码块里那样。 它利用了省略引用的优化,因此模块只在被须要时加载。 为了让这个模块工做,必定要注意 import定义的标识符只能在表示类型处使用(不能在会转换成 JavaScript 的地方)

为了确保类型安全性,咱们可使用typeof关键字。 typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型

// 以下面这样就能够在node.js环境实现可选模块加载
declare function require(moduleName: string): any import { ZipCodeValidator as Zip } from './ZipCodeValidator'

if (needZipValidation) {
  let ZipCodeValidator: typeof Zip = require('./ZipCodeValidator')
  let validator = new ZipCodeValidator()
  if (validator.isAcceptable('...')) {
    /* ... */
  }
}
复制代码

8.5 模块转换问题

TypeScript 中默认是将全部代码转换为CommonJS模块代码,相对于模块有不一样的代码转换规则

  • ES6 模块:

    • import * as ... from ...,这种写法是最接近CommonJSrequire的写法,将全部导出的模块装维一个对象,因此最后也会变为var ... = require('...')

    • import {...} from ...,同上一种同样,不过至关因而用了取对象符

    • import ... from ...,由于这种写法是取出 export 的默认导出,而默认导出实际上是模块的一个叫做default的属性,因此也是用了取对象符var ... = require('...').default

      注意: 这样导入的模块通常是须要对应ES6export default语法的,由于要获取default属性,而使用的CommonJSexport =的写法是直接导出一整个对象,若是不给这些导出的对象设置default属性会获得undefined

    • export单独导入同CommonJS中的exports.xxx语法,只须要主要export default等同于exports.default = xxx

  • CommonJS 模块: 由于是转为这种语法的,因此没有兼容性可说

  • TypeScript 模块:

    • import ... = require('...'),等同于CommonJSrequire语法,只是能够支持 AMD 模块,而原生的require是不支持的
    • export =,等同于CommonJSmodule.exports =

9.命名空间

在代码量较大的状况下,为了不各类变量命名相冲突,能够将相似功能的函数、类、接口等放置到命名空间中

在 TypeScript 中的命名空间中的对象、类、函数等能够经过 export 暴露出来经过命名空间名.类名等来使用

注意: 这个暴露是暴露在命名空间外,不是将其在模块中暴露出去

命名空间和模块的区别:

  • 命名空间: 内部模块,主要用于组织代码,避免命名冲突
  • 模块: TypeScript 的外部模块的简称,侧重代码的复用,一个模块里可能会有多个命名空间
namespace Validation {
  //经过namespace关键词建立一个命名空间
  export interface StringValidator {
    isAcceptable(s: string): boolean //类类型接口
  }

  const lettersRegexp = /^[A-Za-z]+$/
  const numberRegexp = /^[0-9]+$/

  export class LettersOnlyValidator implements StringValidator {
    //要在外部使用必须导出
    isAcceptable(s: string) {
      //函数内部能够不导出
      return lettersRegexp.test(s)
    }
  }

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s)
    }
  }
}

// Some samples to try
let strings = ['Hello', '98052', '101']

// 在外界就能够直接经过Validation.StringValidator访问命名空间内部导出的接口
let validators: { [s: string]: Validation.StringValidator } = {}
//上面接口的意思是一个对象,对象中的每一个成员都是有isAcceptable接口方法的实例化对象
validators['ZIP code'] = new Validation.ZipCodeValidator()
validators['Letters only'] = new Validation.LettersOnlyValidator()

// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${ validators[name].isAcceptable(s) ? 'matches' : 'does not match' } ${name}`
    )
  }
}
复制代码

9.1 多文件中的命名空间

若是命名空间相同,多个文件内部的代码会合并到同一个命名空间中,其实就是使用var声明字重复定义变量,若是内部没有导出的变量依然只能在内部使用,而暴露的变量就会合并

注: 若是导出变量有重名,后面的文件会覆盖掉前面的

  • 经过 export 和 import 进行使用

    //module.ts
    export namespace A {
      interface Animal {
        name: string
        eat(): void
      }
      export class Dog implements Animal {
        name: string
        constructor(theName: string) {
          this.name = theName
        }
        eat(): void {
          console.log(this.name + '吃狗粮')
        }
      }
    }
    复制代码
    // A在JS中就被转换为了一个对象
    import { A } from './module'
    let dog = new A.Dog('狗') //传入命名空间
    dog.eat()
    复制代码
  • 经过三斜线指令引入

    三斜线指令: 包含单个 XML 标签的单行注释,注释的内容会作为编译器指令使用,三斜线引用告诉编译器在编译过程当中要引入的额外的文件

    注意: 三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 若是它们出如今一个语句或声明以后,那么它们会被当作普通的单行注释,而且不具备特殊的涵义

    这里只用///<reference path=""/>,其他用法在 TypeScript 中文文档 查看

    /// <reference path="..." />指令是三斜线指令中最多见的一种,它用于声明文件间的 依赖,三斜线引用告诉编译器在编译过程当中要引入的额外的文件,也就是会引入对应 path 的文件

    //Validation.ts
    namespace Validation {
      export interface StringValidator {
        isAcceptable(s: string): boolean
      }
    }
    复制代码
    //LettersOnlyValidator.ts
    /// <reference path="Validation.ts" />
    namespace Validation {
      const lettersRegexp = /^[A-Za-z]+$/
      export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
          return lettersRegexp.test(s)
        }
      }
    }
    复制代码
    //ZipCodeValidator.ts
    /// <reference path="Validation.ts" />
    namespace Validation {
      const numberRegexp = /^[0-9]+$/
      export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
          return s.length === 5 && numberRegexp.test(s)
        }
      }
    }
    复制代码
    /// <reference path="Validation.ts" />
    /// <reference path="LettersOnlyValidator.ts" />
    /// <reference path="ZipCodeValidator.ts" />
    
    // Some samples to try
    let strings = ['Hello', '98052', '101']
    
    // Validators to use
    let validators: { [s: string]: Validation.StringValidator } = {}
    validators['ZIP code'] = new Validation.ZipCodeValidator()
    validators['Letters only'] = new Validation.LettersOnlyValidator()
    
    // Show whether each string passed each validator
    for (let s of strings) {
      for (let name in validators) {
        console.log(
          `"${s}" - ${ validators[name].isAcceptable(s) ? 'matches' : 'does not match' } ${name}`
        )
      }
    }
    复制代码

9.2 别名

别名是另外一种简化命名空间操做的方法是使用import q = x.y.z给经常使用的对象起一个短的名字,不要与用来加载模块的import x = require('name')语法弄混了,这里的语法是为指定的符号建立一个别名

注: 能够用这种方法为任意标识符建立别名,也包括导入的模块中的对象

namespace Shapes {
  export namespace Polygons {
    export class Triangle {}
    export class Square {}
  }
}

import polygons = Shapes.Polygons //用polygons代替Shapes.Polygons,至关于C语言的define
let sq = new polygons.Square() // Same as "new Shapes.Polygons.Square()"
复制代码

注意:并无使用require关键字,而是直接使用导入符号的限定名赋值,与使用 var类似,但它还适用于类型和导入的具备命名空间含义的符号。 重要的是,对于值来说, import会生成与原始符号不一样的引用,因此改变别名的var值并不会影响原始变量的值

10.TypeScript 中的装饰器

装饰器是一种特殊类型的声明,它可以被附加到类声明,方法,属性或参数上,能够修改类的行为,通俗来说装饰器就是一个方法,能够注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能

装饰器已是 ES7 的标准特性之一

常见的装饰器

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 参数装饰器

装饰器的写法

  • 普通装饰器(没法传参)
  • 装饰器工厂(可传参)

注意: 装饰器是一项实验性特性,由于装饰器只是个将来期待的用法,因此默认是不支持的,若是想要使用就要打开 tsconfig.json 中的experimentalDecorators,不然会报语法错误

命令行:

tsc --target ES5 --experimentalDecorators
复制代码

tsconfig.json:

10.1 类装饰器

类装饰器在类声明以前被声明(紧跟着类声明),类装饰器应用于类构造函数,能够用来监视,修改或替换类定义,须要传入一个参数

10.1.1 普通装饰器

function logClass(target: any) {
  console.log(target)
  //target就是当前类,在声明装饰器的时候会被默认传入
  target.prototype.apiUrl = '动态扩展的属性'
  target.prototype.run = function () {
    console.log('动态扩展的方法')
  }
}

@logClass
class HttpClient {
  constructor() {}
  getData() {}
}
//这里必需要设置any,由于是装饰器动态加载的属性,因此在外部校验的时候并无apiUrl属性和run方法
let http: any = new HttpClient()
console.log(http.apiUrl)
http.run()
复制代码

10.1.2 装饰器工厂

若是要定制一个修饰器如何应用到一个声明上,须要写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用

注: 装饰器工厂是将内部调用的函数做为真正的装饰器返回的,因此装饰器工厂须要和函数用法同样经过()来调用,内部能够接收参数

function color(value: string) {
  // 这是一个装饰器工厂
  return function (target: any) {
    //这是装饰器,这个装饰器就是上面普通装饰器默认传入的类
    // do something with "target" and "value"...
  }
}
复制代码
function logClass(value: string) {
  return function (target: any) {
    console.log(target)
    console.log(value)
    target.prototype.apiUrl = value //将传入的参数进行赋值
  }
}

@logClass('hello world') //可传参数的装饰器
class HttpClient {
  constructor() {}
  getData() {}
}

let http: any = new HttpClient()
console.log(http.apiUrl)
复制代码

10.1.3 类装饰器重构构造函数

类装饰器表达式会在运行时看成函数被调用,类的构造函数做为其惟一的参数 ,若是类装饰器返回一个值,它会使用提供的构造函数来替换类的声明,经过这种方法咱们能够很轻松的继承和修改原来的父类,定义本身的属性和方法

注意: 若是要返回一个新的构造函数,必须注意处理好原来的原型链

/*
经过返回一个继承的类实现一个类的属性和方法的重构,换句话说就是在中间层有一个阻拦,而后返回的是一个新的继承了父类的类,这个类必须有父类的全部属性和方法,否则会报错
*/
function logClass(target: any) {
  // 返回一个继承原来类的新的类
  return class extends target {
    //能够当作是固定写法吧
    apiUrl: string = '我是修改后的数据'
    getData() {
      console.log(this.apiUrl)
    }
  }
}
//重构属性和方法
@logClass
class HttpClient {
  // 若是不在这声明TypeScript的检测器检测不出来,在下面的使用都会报错,可使用接口的声明合并来消除
  constructor(public apiUrl = '我是构造函数中的数据') {}
  getData() {
    console.log(123)
  }
}
/*
    interface HttpClient {
      apiUrl: string
      getData(): void
    }
*/

let http: any = new HttpClient()
console.log(http.apiUrl) //我是修改后的数据
http.getData() //我是修改后的数据
复制代码

10.1.4 装饰器求值

类中不一样声明上的装饰器将按如下规定的顺序应用:

  1. 参数装饰器,而后依次是方法装饰器访问符装饰器,或属性装饰器应用到每一个实例成员
  2. 参数装饰器,而后依次是方法装饰器访问符装饰器,或属性装饰器应用到每一个静态成员
  3. 参数装饰器应用到构造函数
  4. 类装饰器应用到类

10.2 方法装饰器

方法装饰器声明在一个方法的声明以前(紧靠着方法声明)

注意:

  • 它会被应用到方法的属性描述符上,能够用来监视,修改或者替换方法定义
  • 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(好比declare的类)中

方法装饰器被应用到方法的属性描述符上,能够用来监视,修改或替换方法定义,传入三个参数(都是自动传入的):

  • 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象

  • 成员的名字(只是个 string 类型的字符串,没有其他做用)

  • 成员的属性描述符,是一个对象,里面有真正的方法自己

    注: 若是代码输出目标版本小于ES5,属性描述符将会是undefined

注意:若是方法装饰器返回一个值,它会被用做方法的属性描述符,若是代码输出目标版本小于ES5返回值会被忽略

function get(value: any) {
  // PropertyDescriptor是TypeScript中内置的属性描述符的类型限定,包含了类型修辞符的全部属性
  return function (target: any, methodName: string, desc: PropertyDescriptor) {
    console.log(target) //HttpClient类
    console.log(methodName) //getData方法名,一个字符串
    console.log(desc) //描述符
    console.log(desc.value) //方法自己就在desc.value中
    target.url = 123 //也能改变原实例
  }
}

class HttpClient {
  public url: any | undefined
  constructor() {}
  @get('hello world')
  getData() {
    console.log(this.url)
  }
}

let http = new HttpClient()
console.log(http.url) //123
复制代码
function get(value: any) {
  // PropertyDescriptor是TypeScript中内置的属性描述符的类型限定
  return function (target: any, methodName: string, desc: PropertyDescriptor) {
    let oMethod = desc.value
    desc.value = function (...args: any[]) {
      //由于用了方法装饰器,因此实际调用getData()方法的时候会调用desc.value来实现,经过赋值能够实现重构方法
      //原来的方法已经赋值给oMethod了,因此能够改变
      args = args.map(
        //这个段代码是将传入的参数所有转换为字符串
        (value: any): string => {
          return String(value)
        }
      )
      console.log(args) //由于方法重构了,因此原来的getData()中的代码无效了,调用时会打印转换后参数
      /* 若是想依然能用原来的方法,那么写入下面的代码,至关于就是对原来的方法进行了扩展 */
      oMethod.apply(target, args) //经过这种方法调用能够也实现原来的getData方法
    }
  }
}

class HttpClient {
  public url: any | undefined
  constructor() {}
  @get('hello world')
  getData(...args: any[]) {
    console.log(args) //[ '1', '2', '3', '4', '5', '6' ]
    console.log('我是getData中的方法')
  }
}

let http = new HttpClient()
http.getData(1, 2, 3, 4, 5, 6) //[ '1', '2', '3', '4', '5', '6' ]
复制代码
function get(bool: boolean): any {
  return (target: any, prop: string, desc: PropertyDescriptor) => {
    // 经过返回值修改属性描述符
    return {
      value() {
        return 'not age'
      },
      enumerable: bool
    }
  }
}

class Test {
  constructor(public age: number) {}
  @get(false)
  public getAge() {
    return this.age
  }
}
const t = new Test(18)
console.log(t.getAge()) // not age,getAge()函数的值以及被修改了
for (const key in t) {
  console.log(key) // 只有age属性,若是上面@get传入的是true就还有getAge()方法
}
复制代码

10.3.1 属性描述符

在 ES5 以前,JavaScript 没有内置的机制来指定或者检查对象某个属性(property)的特性(characteristics),好比某个属性是只读(readonly)的或者不能被枚举(enumerable)的。可是在 ES5 以后,JavaScript 被赋予了这个能力,全部的对象属性均可以经过属性描述符(Property Descriptor)来指定

interface obj {
  [key: string]: any
}
let myObject: obj = {}

Object.defineProperty(myObject, 'a', {
  value: 2,
  writable: true, // 可写
  configurable: true, // 可配置
  enumerable: true // 可遍历
})
// 上面的定义等同于 myObject.a = 2;
// 因此若是不须要修改这三个特性,咱们不会用 `Object.defineProperty`

console.log(myObject.a) // 2
复制代码

属性描述符的六个属性

  • value:属性值

  • writable:是否容许赋值,true 表示容许,不然该属性不容许赋值

    interface obj {
      [key: string]: any
    }
    let myObject: obj = {}
    
    Object.defineProperty(myObject, 'a', {
      value: 2,
      writable: false, // 不可写
      configurable: true,
      enumerable: true
    })
    
    myObject.a = 3 // 写入的值将会被忽略
    console.log(myObject.a) // 2
    复制代码
  • get:返回属性值的函数。若是为 undefined 则直接返回描述符中定义的 value

  • set:属性的赋值函数。若是为 undefined 则直接将赋值运算符右侧的值保存为属性值

    注:

    • 一旦同时使用了getset,须要一个中间变量存储真正的值。
    • setwritable:false是不能共存的。
  • configurable:若是为 true,则表示该属性能够从新使用(Object.defineProperty(...) )定义描述符,或者从属性的宿主删除。缺省为 true

    let myObject = {
      a: 2
    }
    
    Object.defineProperty(myObject, 'a', {
      value: 4,
      writable: true,
      configurable: false, // 不可配置!
      enumerable: true
    })
    
    console.log(myObject.a) // 4
    myObject.a = 5
    // 由于最开始writable时true,因此不会影响到赋值
    console.log(myObject.a) // 5
    
    Object.defineProperty(myObject, 'a', {
      value: 6,
      writable: true,
      configurable: true,
      enumerable: true
    }) // TypeError
    复制代码

    注: 一旦某个属性被指定为 configurable: false,那么就不能重新指定为configurable: true 了,这个操做是单向,不可逆的

    这个特性还会影响delete 操做的行为

    let myObject = {
      a: 2
    }
    
    Object.defineProperty(myObject, 'a', {
      value: 4,
      writable: true,
      configurable: false, // 不可配置!
      enumerable: true
    })
    delete myObject.a
    console.log(myObject.a) // 4
    复制代码
  • enumerable:若是为 true,则表示遍历宿主对象时,该属性能够被遍历到(好比 for..in 循环中)。缺省为 true

    interface obj {
      [key: string]: any
    }
    let myObject: obj = {}
    
    Object.defineProperty(
      myObject,
      'a',
      // make `a` enumerable, as normal
      { enumerable: true, value: 2 }
    )
    
    Object.defineProperty(
      myObject,
      'b',
      // make `b` NON-enumerable
      { enumerable: false, value: 3 }
    )
    console.log(myObject.b) // 3
    console.log('b' in myObject) // true
    myObject.hasOwnProperty('b') // true
    
    // .......
    // 没法被遍历到
    for (let k in myObject) {
      console.log(k, myObject[k])
    }
    // "a" 2
    
    myObject.propertyIsEnumerable('a') // true
    myObject.propertyIsEnumerable('b') // false
    
    Object.keys(myObject) // ["a"]
    Object.getOwnPropertyNames(myObject) // ["a", "b"]
    复制代码

    能够看出,enumerable: false 使得该属性从对象属性枚举操做中被隐藏,但Object.hasOwnProperty(...) 仍然能够检测到属性的存在。另外,Object.propertyIsEnumerable(..) 能够用来检测某个属性是否可枚举,Object.keys(...) 仅仅返回可枚举的属性,而Object.getOwnPropertyNames(...) 则返回该对象上的全部属性,包括不可枚举的

注: Object 有专门操做属性的方法,在这里就再也不多讲了

10.3 方法参数装饰器

参数装饰器声明在一个参数声明以前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。

注意: 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(好比 declare的类)里

参数装饰器被表达式会在运行时看成函数被调用,可使用参数装饰器为类的原型增长一些元素数据,传入三个参数(都是自动传入的):

  • 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象
  • 方法的名字(只是个 string 类型的字符串,没有其他做用)
  • 参数在函数参数列表中的索引

注:

  • 参数装饰器只能用来监视一个方法的参数是否被传入
  • 参数装饰器的返回值会被忽略
//这个装饰器不多使用
function logParams(value: any) {
  return function (target: any, methodName: any, paramsIndex: any) {
    console.log(target)
    console.log(methodName) //getData
    console.log(paramsIndex) //1,由于value在下面是第二个参数
  }
}

class HttpClient {
  public url: any | undefined
  constructor() {}
  getData(index: any, @logParams('hello world') value: any) {
    console.log(index)
    console.log(value)
  }
}

let http: any = new HttpClient()
http.getData(0, '123') //我是修改后的数据
复制代码

10.4 访问器装饰器

访问器装饰器声明在一个访问器的声明以前(紧靠着访问器声明)。 访问器装饰器应用于访问器的属性描述符而且能够用来监视,修改或替换一个访问器的定义。

注意:

  • 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(好比 declare的类)里
  • TypeScript 不容许同时装饰一个成员的getset访问器。取而代之的是,一个成员的全部装饰的必须应用在文档顺序的第一个访问器上。这是由于,在装饰器应用于一个属性描述符时,它联合了getset访问器,而不是分开声明的

访问器装饰器表达式会在运行时看成函数被调用,传入下列 3 个参数(都是自动传入的):

  • 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象

  • 成员的名字

  • 成员的属性描述符

    注: 若是代码输出目标版本小于ES5Property Descriptor将会是undefined

注意: 若是访问器装饰器返回一个值,它会被用做方法的属性描述符。若是代码输出目标版本小于ES5返回值会被忽略

function configurable(value: boolean) {
  return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) {
    descriptor.configurable = value
  }
}

class Point {
  private _x: number
  private _y: number
  constructor(x: number, y: number) {
    this._x = x
    this._y = y
  }

  @configurable(false)
  get x() {
    return this._x
  }

  @configurable(false)
  get y() {
    return this._y
  }
}
复制代码

10.5 属性装饰器

属性装饰器声明在一个属性声明以前(紧靠着属性声明)。

注意: 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(好比 declare的类)里。

属性装饰器表达式在运行时看成函数被调用,传入两个参数(都是自动传入的):

  • 对应静态成员来讲是类的构造函数,对于实例成员来讲是类的原型对象
  • 成员的名字

注: 属性描述符不会作为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。 由于目前没有办法在定义一个原型对象的成员时描述一个实例属性,而且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。 所以,属性描述符只能用来监视类中是否声明了某个名字的属性

function logProperty(value: string) {
  return function (target: any, attr: string) {
    //target为实例化的成员对象,attr为下面紧挨着的属性
    console.log(target)
    console.log(attr)
    target[attr] = value //能够经过修饰器改变属性的值
  }
}

class HttpClient {
  @logProperty('hello world') //修饰器后面紧跟着对应要修饰的属性
  public url: string | undefined
  constructor() {}
  getData() {
    console.log(this.url)
  }
}

let http: any = new HttpClient()
http.getData() //hello world
复制代码

10.5.5 返回值总结

  • 属性和方法参数装饰器的返回值会被忽略
  • 访问器和方法装饰器的返回值都会被用作方法的属性描述符(低于Es5版本会被忽略)
  • 类装饰器的返回值会返回一个新的构造函数

10.6 装饰器的执行顺序

咱们能够对同一个对象使用多个装饰器,装饰器的执行顺序是从后往前执行的

  • 书写在同一行上

    @f @g x
    复制代码
  • 书写在多行上

    @f
    @g
    x
    复制代码

在 TypeScript 里,当多个装饰器应用在一个声明上时会进行以下步骤的操做:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被看成函数,由下至上依次调用。

简单的说就是: 若是是装饰器工厂修饰的(不是只有一个函数,是经过返回函数来实现),会从上到下按照代码的顺序先执行装饰器工厂生成装饰器,而后再从下往上执行装饰器

特别提醒: 若是方法和方法参数装饰器在同一个方法出现,参数装饰器先执行

function f() {
  console.log('f(): evaluated')
  return function ( target, propertyKey: string, descriptor: PropertyDescriptor ) {
    console.log('f(): called')
  }
}

function g() {
  console.log('g(): evaluated')
  return function ( target, propertyKey: string, descriptor: PropertyDescriptor ) {
    console.log('g(): called')
  }
}

class C {
  @f()
  @g()
  method() {}
}
复制代码
# 在控制台中打印
f(): evaluated
g(): evaluated
g(): called
f(): called
复制代码

11.Mixins 混入

11.1 对象的混入

和 JS 同样,TypeScript 中混入对象也是使用Object.assign()方法来实现,很少最后的结果会多了一个交叉类型的类型定义,同时包含了全部混入对象的属性

interface ObjectA {
  a: string
}

interface ObjectB {
  b: string
}

let A: ObjectA = {
  a: 'a'
}

let B: ObjectB = {
  b: 'b'
}

let AB: ObjectA & ObjectB = Object.assign(A, B) // 及时左边没有类型定义也会自动被定义为交叉类型
console.log(AB)
复制代码

11.2 类的混入

对于类的混入,咱们须要理解下面这个例子:

// Disposable Mixin
class Disposable {
  isDisposed: boolean
  dispose() {
    this.isDisposed = true
  }
}

// Activatable Mixin
class Activatable {
  isActive: boolean
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}

class SmartObject implements Disposable, Activatable {
  constructor() {
    setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500)
  }

  interact() {
    this.activate()
  }

  // Disposable
  isDisposed: boolean = false
  dispose: () => void
  // Activatable
  isActive: boolean = false
  activate: () => void
  deactivate: () => void
}
applyMixins(SmartObject, [Disposable, Activatable])

let smartObj = new SmartObject()
setTimeout(() => smartObj.interact(), 1000)

////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      derivedCtor.prototype[name] = baseCtor.prototype[name]
    })
  })
}
复制代码

代码里首先定义了两个类,它们将作为 mixins。 能够看到每一个类都只定义了一个特定的行为或功能。 稍后咱们使用它们来建立一个新类,同时具备这两种功能

// Disposable Mixin
class Disposable {
  isDisposed: boolean
  dispose() {
    this.isDisposed = true
  }
}

// Activatable Mixin
class Activatable {
  isActive: boolean
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}
复制代码

而后咱们须要建立一个类来使用他们做为接口进行限制。没使用extends而是使用implements。 把类当成了接口,仅使用 Disposable 和 Activatable 的类型而非其实现。 这意味着咱们须要在类里面实现接口。 可是这是咱们在用 mixin 时想避免的。

咱们能够这么作来达到目的,为将要 mixin 进来的属性方法建立出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用 mixin 带来的便利,虽然说须要提早定义一些占位属性。

class SmartObject implements Disposable, Activatable {
  constructor() {
    setInterval(() => console.log(this.isActive + ' : ' + this.isDisposed), 500)
  }

  interact() {
    this.activate()
  }

  // Disposable
  isDisposed: boolean = false
  dispose: () => void
  // Activatable
  isActive: boolean = false
  activate: () => void
  deactivate: () => void
}
复制代码

建立帮助函数,帮咱们作混入操做。 它会遍历 mixins 上的全部属性,并复制到目标上去,把以前的占位属性替换成真正的实现代码

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      derivedCtor.prototype[name] = baseCtor.prototype[name]
    })
  })
}
复制代码

最后,把 mixins 混入定义的类,完成所有实现部分

applyMixins(SmartObject, [Disposable, Activatable])
复制代码

总结

想了想,最后仍是加个总结吧,第一次使用 TypeScript 实际上是在 3.1 版本发行的时候,当初因为对强类型语言没有什么经验(当初只学了 C,也没有学习 Java 等),对当时的我来讲,从 JavaScript 直接转向 TypeScript 是件很是困难的事,因此这个汇总笔记的时间跨度其实仍是比较大的,中间通过不断修修补补,也算是对 TypeScript 有了必定的理解。到现在个人项目中也都是使用 TypeScript 进行开发,也算是不妄我这么长的笔记吧(笑)。

最后的最后,现在 TypeScript 已经成为了前端的一大趋势,掌握 TypeScript 也逐渐变成了前端开发者们的基本技能,花一点时间对 TypeScript 进行深刻了解可以写出更加符合规范的代码,对项目的开发与维护都有着极大的做用。 若是你对 TypeScript 有着本身的见解或笔记存在的不完备的地方,欢迎在评论区留言。

更多内容

TypeScript 知识汇总(一)(3W 字长文)

TypeScript 知识汇总(二)(3W 字长文)

TypeScript 知识汇总(三)(3W 字长文)

相关文章
相关标签/搜索