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 字長文)

相關文章
相關標籤/搜索