随着应用的庞大,项目中 JavaScript 的代码也会愈来愈臃肿,这时候许多 JavaScript 的语言弊端就会愈发明显,而 TypeScript 的出现,就是着力于解决 JavaScript 语言天生的弱势:静态类型。html
前端开发 QQ 群:377786580前端
这篇文章首发于个人我的博客 《据说》,系列目录:vue
在上一篇文章 《从 JavaScript 到 TypeScript 3 - 引入和编译》 咱们简单介绍了 TypeScript 的引入和编译,在这篇文章中,咱们会讨论 ECMAScript 的新特性,为后续的内容作点铺垫。node
在了解装饰器以前,咱们先看一段代码:express
class User { name: string id: number constructor(name:string, id: number) { this.name = name this.id = id } changeName (newName: string) { this.name = newName } }
这段代码声明了一个 Class 为 User
,User
提供了一个实例方法 changeName()
用来修改字段 name
的值。npm
如今咱们要在修改 name
以前,先对 newName
作校验,判断若是 newName
的值为空字符串,就抛出异常。json
按照咱们过去的作法,咱们会修改 changeName()
函数,或者提供一个 validaName()
方法:bash
class User { name: string id: number constructor(name:string, id: number) { this.name = name this.id = id } // 验证 Name validateName (newName: string) { if (!newName){ throw Error('name is invalid') } } changeName (newName: string) { // 若是 newName 为空字符串,则会抛出异常 this.validateName(newName) this.name = newName } }
能够看到,咱们新编写的 validateName()
,侵入到了 changeName()
的逻辑中。如此带来一个弊端:函数
changeName()
里面可能还包含了什么样的隐性逻辑changeName()
被扩展后逻辑不清晰而后咱们把调用时机从 changeName()
中抽出来,先调用 validateName()
,再调用 changeName()
:ui
let user = new User('linkFly', 1) if (user.validateName('tasaid')) { user.changeName('tasaid') }
可是上面的问题 1 仍然没有被解决,调用方代码变的十分啰嗦。那么有没有更好的方式来表现这层逻辑呢?
装饰器就用来解决这个问题:"无侵入式" 的加强。
顾名思义,"装饰器" (也叫 "注解")就是对一个 类/方法/属性/参数 的装饰。它是对这一系列代码的加强,而且经过自身描述了被装饰的代码可能存在的行为改变。
简单来讲,装饰器就是对代码的描述。
因为装饰器是实验性特性,因此要在 tsconfig.json
里启用这个实验性特性:
{ "compilerOptions": { // 支持装饰器 "experimentalDecorators": true, } }
钢铁侠托尼·史塔克只是一个有血有肉的人,而他的盔甲让他成为了钢铁侠,盔甲就是对托尼·史塔克的装饰(加强)。
咱们使用装饰器修改一下上面的例子:
// 声明一个装饰器,第三个参数是 "成员的属性描述符",若是代码输出目标版本(target)小于 ES5 返回值会被忽略。 const validate = function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { // 保存原来的方法 let method = descriptor.value // 重写原来的方法 descriptor.value = (newValue: string) => { // 检查是不是空字符串 if (!newValue) { throw Error('name is invalid') } else { // 不然调用原来的方法 method() } } } class User { name: string id: number constructor(name:string, id: number) { this.name = name this.id = id } // 调用装饰器 @validate changeName (newName: string) { this.name = newName } }
这里咱们能够看到,changeName
的逻辑没有任何改变,但其实它的行为已经经过装饰器 @validate
加强。
这就是装饰器的做用。装饰器能够用很直观的方式来描述代码:
class User { name: string @validateString set name (@required name: string) { this.name = name } }
装饰器的执行时机以下:
// 这是一个装饰器工厂,在外面使用 @god() 的时候就会调用这个工厂 function god(name: string) { console.log(`god(): evaluated ${name}`) // 这是装饰器,在 User 生成以后会执行 return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log('god(): called') } } class User { @god('test') test () { } }
以上代码输出结果
god(): evaluated test god(): called
咱们也能够直接声明一个装饰器来使用(要注意和装饰器工厂的区别):
function god(target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("god(): called") } class User { // 注意这里不是 @god(),没有 () @god test () { } }
装饰器家族有 4 种装饰形式,注意,装饰器能装饰在类、方法、属性和参数上,但不能只装饰在函数上!
类装饰器表达式会在运行时看成函数被调用,类的构造函数做为其惟一的参数。
function sealed(constructor: Function) { Object.seal(constructor) Object.seal(constructor.prototype) } @sealed class User { }
方法装饰器表达式会在运行时看成函数被调用,传入下列 3个参数:
{value: any, writable: boolean, enumerable: boolean, configurable: boolean}
function god(name: string) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) { // target: 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象 // propertyKey: 成员的名字 // descriptor: 成员的属性描述符 {value: any, writable: boolean, enumerable: boolean, configurable: boolean} } } class User { @god('tasaid.com') sayHello () { } }
和函数装饰器同样,只不过是装饰于访问器上的。
function god(name: string) { return function (target, propertyKey: string, descriptor: PropertyDescriptor) { // target: 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象 // propertyKey: 成员的名字 // descriptor: 成员的属性描述符 {value: any, writable: boolean, enumerable: boolean, configurable: boolean} } } class User { private _name: string // 装饰在访问器上 @god('tasaid.com') get name () { return this._name } }
属性装饰器表达式会在运行时看成函数被调用,传入下列 2个参数:
function god(target, propertyKey: string) { // target: 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象 // propertyKey: 成员的名字 } class User { @god name: string }
参数装饰器表达式会在运行时看成函数被调用,传入下列 3个参数:
const required = function (target, propertyKey: string, parameterIndex: number) { // target: 对于静态成员来讲是类的构造函数,对于实例成员是类的原型对象 // propertyKey: 成员的名字 // parameterIndex: 参数在函数参数列表中的索引 } class User { private _name : string; set name(@required name : string) { this._name = name; } }
例如上面 validate
的例子能够用在参数装饰器上
// 定义一个私有 key const requiredMetadataKey = Symbol("required") // 定义参数装饰器,大概思路就是把要校验的参数索引保存到成员中 const required = function (target, propertyKey: string, parameterIndex: number) { // 参数装饰器只能拿到参数的索引 if (!target[propertyKey][requiredMetadataKey]) { target[propertyKey][requiredMetadataKey] = {} } // 把这个索引挂到属性上 target[propertyKey][requiredMetadataKey][parameterIndex] = true } // 定义一个方法装饰器,从成员中获取要校验的参数进行校验 const validateEmptyStr = function (target, propertyKey: string, descriptor: PropertyDescriptor) { // 保存原来的方法 let method = descriptor.value // 重写原来的方法 descriptor.value = function () { let args = arguments // 看当作员里面有没有存的私有的对象 if (target[propertyKey][requiredMetadataKey]) { // 检查私有对象的 key Object.keys(target[propertyKey][requiredMetadataKey]).forEach(parameterIndex => { // 对应索引的参数进行校验 if (!args[parameterIndex]) throw Error(`arguments${parameterIndex} is invalid`) }) } } } class User { name: string id: number constructor(name:string, id: number) { this.name = name this.id = id } // 方法装饰器作校验 @validateEmptyStr changeName (@required newName: string) { // 参数装饰器作描述 this.name = newName } }
反射,就是在运行时动态获取一个对象的一切信息:方法/属性等等,特色在于动态类型反推导。在 TypeScript 中,反射的原理是经过设计阶段对对象注入元数据信息,在运行阶段读取注入的元数据,从而获得对象信息。
反射能够获取对象的:
class User { name: string = 'linkFly' say (myName: string): string { return `hello, ${myName}` } }
例如上面的例子,在 TypeScript 中能够获取到这些信息:
User
User
有一个属性名为 name
,有一个方法 say()
name
是 string
类型的,且值为 linkFly
say()
接受一个 string
类型的参数,在 TypeScript 中,参数名是获取不到的 say()
返回类型为 string
TypeScript 结合自身静态类型语言的特色,为使用了装饰器的代码声明注入了 3 组元数据:
design:type
: 成员类型design:paramtypes
: 成员全部参数类型design:returntype
: 成员返回类型因为元数据反射也是实验性 API,因此要在 tsconfig.json
里启用这个实验性特性:
{ "compilerOptions": { "target": "ES5", // 支持装饰器 "experimentalDecorators": true, // 装饰器元数据 "emitDecoratorMetadata": true } }
而后安装 reflect-metadata
npm i reflect-metadata --save
这样在装饰器中,就能够访问到由 TypeScript 注入的基本信息元数据:
import 'reflect-metadata' let meta = function (target: any, propertyKey: string) { // 获取成员类型 let type = Reflect.getMetadata('design:type', target, propertyKey) // 获取成员参数类型 let paramtypes = Reflect.getMetadata('design:paramtypes', target, propertyKey) // 获取成员返回类型 let returntype = Reflect.getMetadata('design:returntype', target, propertyKey) // 获取全部元数据 key (由 TypeScript 注入) let keys = Reflect.getMetadataKeys(target, propertyKey) console.log(keys) // [ 'design:returntype', 'design:paramtypes', 'design:type' ] // 成员类型 console.log(type) // Function // 参数类型 console.log(paramtypes) // [String] // 成员返回类型 console.log(returntype) // String } class User { // 使用这个装饰器就能够反射出成员详细信息 @meta say (myName: string): string { return `hello, ${myName}` } }
Java 和 C# 因为是强类型编译型语言,因此反射就成了它们动态反推导数据类型的一个重要特性。
目前来讲,JavaScript 由于其动态性,因此自己就包含了一些反射的特色:
TypeScript 补充了基础的类型元数据,只不过仍是有些地方不够完善:在 TypeScript 中,参数名经过反射是获取不到的。
为何获取不到呢?由于 JavaScript 本质上仍是解释型语言,还迎合 Web 有一大特点:编译和压缩...
User_1
myName
可能叫 m
angular 1.x
中使用的依赖注入,采用传字符串那么蹩脚的方式,也是对 JavaScript 反射机制的不完善作出的一种妥协。
在下一篇《从 JavaScript 到 TypeScript 5 - express 路由进化》 中,咱们将在 express 上,使用装饰器和反射实现全新的路由表现。
TypeScript 中文网:https://tslang.cn/
TypeScript 视频教程:《TypeScript 精通指南》