文章使用的 TypeScript 版本爲
3.9.x
,後續會根據 TypeScript 官方的更新繼續添加內容,若是有的地方不同多是版本報錯的問題,注意對應版本修改便可。html
該文章是筆者在學習 TypeScript 的筆記總結,期間尋求了許多資源,包括 TypeScript 的官方文檔等多方面內容,因爲技術緣由,可能有不少總結錯誤或者不到位的地方,還請諸位及時指正,我會在第一時間做出修改。前端
文章中許多部分的展現順序並非按照教程順序,只是對於同一類型的內容進行了分類處理,許多特性可能會提早使用,若是遇到不懂的地方能夠先看後面內容。node
下面內容接 TypeScript 知識彙總(二)(3W 字長文)react
與 ES6 同樣,TypeScript 也引入了模塊化的概念,TypeScript 也可使用 ES6 中的 export、export default 和 import 導出和引入模塊類的數據,從而實現模塊化git
ES6 標準與 Common.js 的區別es6
注: ES6 的模塊不是對象,import
命令會被 JavaScript 引擎靜態分析,在編譯時就引入模塊代碼,而不是在代碼運行時加載,因此沒法實現條件加載github
任何聲明(好比變量、函數、類、類型別名或接口)都可以經過添加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)
}
}
複製代碼
//上面的語句能夠直接經過導出語句來寫
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可以改變導出變量的名字,在外部接收時使用
複製代碼
每一個模塊均可以有一個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"
複製代碼
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'}`
)
})
複製代碼
模塊的導入操做與導出同樣簡單,可使用如下 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'
複製代碼
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)
})
複製代碼
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.
// ...
}
複製代碼
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
同樣基本都是用於導出一個對象出來ES6
的import ... from ...
的默認導出的語法不能做用在export =
導出的對象,由於沒有default
對象,就像CommonJS
的module.exports
同樣(雖然最後是轉換爲這個),而ES6
的export 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'}`
)
})
複製代碼
在以前說到的import module = require("module")
的區別的緣由是根據編譯時指定的模塊目標參數,編譯器會生成相應的供 Node.js (CommonJS),Require.js (AMD),UMD,SystemJS或ECMAScript 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
複製代碼
有時候,你只想在某種條件下才加載某個模塊。 在 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('...')) {
/* ... */
}
}
複製代碼
TypeScript 中默認是將全部代碼轉換爲CommonJS
模塊代碼,相對於模塊有不一樣的代碼轉換規則
ES6 模塊:
import * as ... from ...
,這種寫法是最接近CommonJS
中require
的寫法,將全部導出的模塊裝維一個對象,因此最後也會變爲var ... = require('...')
import {...} from ...
,同上一種同樣,不過至關因而用了取對象符
import ... from ...
,由於這種寫法是取出 export 的默認導出,而默認導出實際上是模塊的一個叫做default
的屬性,因此也是用了取對象符var ... = require('...').default
注意: 這樣導入的模塊通常是須要對應ES6
的export default
語法的,由於要獲取default
屬性,而使用的CommonJS
和export =
的寫法是直接導出一整個對象,若是不給這些導出的對象設置default
屬性會獲得undefined
export
單獨導入同CommonJS
中的exports.xxx
語法,只須要主要export default
等同於exports.default = xxx
CommonJS 模塊: 由於是轉爲這種語法的,因此沒有兼容性可說
TypeScript 模塊:
import ... = require('...')
,等同於CommonJS
的require
語法,只是能夠支持 AMD 模塊,而原生的require
是不支持的export =
,等同於CommonJS
的module.exports =
在代碼量較大的狀況下,爲了不各類變量命名相沖突,能夠將相似功能的函數、類、接口等放置到命名空間中
在 TypeScript 中的命名空間中的對象、類、函數等能夠經過 export 暴露出來經過命名空間名.類名
等來使用
注意: 這個暴露是暴露在命名空間外,不是將其在模塊中暴露出去
命名空間和模塊的區別:
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}`
)
}
}
複製代碼
若是命名空間相同,多個文件內部的代碼會合併到同一個命名空間中,其實就是使用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}`
)
}
}
複製代碼
別名是另外一種簡化命名空間操做的方法是使用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
值並不會影響原始變量的值
裝飾器是一種特殊類型的聲明,它可以被附加到類聲明,方法,屬性或參數上,能夠修改類的行爲,通俗來說裝飾器就是一個方法,能夠注入到類、方法、屬性參數上來擴展類、屬性、方法、參數的功能
裝飾器已是 ES7 的標準特性之一
常見的裝飾器
裝飾器的寫法
注意: 裝飾器是一項實驗性特性,由於裝飾器只是個將來期待的用法,因此默認是不支持的,若是想要使用就要打開 tsconfig.json 中的experimentalDecorators
,不然會報語法錯誤
命令行:
tsc --target ES5 --experimentalDecorators
複製代碼
tsconfig.json:
類裝飾器在類聲明以前被聲明(緊跟着類聲明),類裝飾器應用於類構造函數,
能夠用來監視,修改或替換類定義,須要傳入一個參數
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()
複製代碼
若是要定製一個修飾器如何應用到一個聲明上,須要寫一個裝飾器工廠函數。 裝飾器工廠就是一個簡單的函數,它返回一個表達式,以供裝飾器在運行時調用
注: 裝飾器工廠是將內部調用的函數做爲真正的裝飾器返回的,因此裝飾器工廠須要和函數用法同樣經過()
來調用,內部能夠接收參數
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)
複製代碼
類裝飾器表達式會在運行時看成函數被調用,類的構造函數做爲其惟一的參數 ,若是類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明,經過這種方法咱們能夠很輕鬆的繼承和修改原來的父類,定義本身的屬性和方法
注意: 若是要返回一個新的構造函數,必須注意處理好原來的原型鏈
/*
經過返回一個繼承的類實現一個類的屬性和方法的重構,換句話說就是在中間層有一個阻攔,而後返回的是一個新的繼承了父類的類,這個類必須有父類的全部屬性和方法,否則會報錯
*/
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() //我是修改後的數據
複製代碼
類中不一樣聲明上的裝飾器將按如下規定的順序應用:
方法裝飾器聲明在一個方法的聲明以前(緊靠着方法聲明)
注意:
.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()方法
}
複製代碼
在 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 則直接將賦值運算符右側的值保存爲屬性值
注:
get
和set
,須要一箇中間變量存儲真正的值。set
和writable: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 有專門操做屬性的方法,在這裏就再也不多講了
參數裝飾器聲明在一個參數聲明以前(緊靠着參數聲明)。 參數裝飾器應用於類構造函數或方法聲明。
注意: 參數裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(好比 declare
的類)裏
參數裝飾器被表達式會在運行時看成函數被調用,可使用參數裝飾器爲類的原型增長一些元素數據,傳入三個參數(都是自動傳入的):
注:
//這個裝飾器不多使用
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') //我是修改後的數據
複製代碼
訪問器裝飾器聲明在一個訪問器的聲明以前(緊靠着訪問器聲明)。 訪問器裝飾器應用於訪問器的屬性描述符而且能夠用來監視,修改或替換一個訪問器的定義。
注意:
declare
的類)裏get
和set
訪問器。取而代之的是,一個成員的全部裝飾的必須應用在文檔順序的第一個訪問器上。這是由於,在裝飾器應用於一個屬性描述符時,它聯合了get
和set
訪問器,而不是分開聲明的訪問器裝飾器表達式會在運行時看成函數被調用,傳入下列 3 個參數(都是自動傳入的):
對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象
成員的名字
成員的屬性描述符
注: 若是代碼輸出目標版本小於ES5
,Property 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
}
}
複製代碼
屬性裝飾器聲明在一個屬性聲明以前(緊靠着屬性聲明)。
注意: 屬性裝飾器不能用在聲明文件中(.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
複製代碼
Es5
版本會被忽略)咱們能夠對同一個對象使用多個裝飾器,裝飾器的執行順序是從後往前執行的
書寫在同一行上
@f @g x
複製代碼
書寫在多行上
@f
@g
x
複製代碼
在 TypeScript 裏,當多個裝飾器應用在一個聲明上時會進行以下步驟的操做:
簡單的說就是: 若是是裝飾器工廠修飾的(不是隻有一個函數,是經過返回函數來實現),會從上到下按照代碼的順序先執行裝飾器工廠生成裝飾器,而後再從下往上執行裝飾器
特別提醒: 若是方法和方法參數裝飾器在同一個方法出現,參數裝飾器先執行
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
複製代碼
和 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)
複製代碼
對於類的混入,咱們須要理解下面這個例子:
// 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 有着本身的見解或筆記存在的不完備的地方,歡迎在評論區留言。