文章使用的 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 有着本身的见解或笔记存在的不完备的地方,欢迎在评论区留言。