闲言
一切都要从公司里的一位老哥给我看的一段代码说起。。。
@controller('/user')
@auth
@post('/login')
async userLogin = (name, pass) => {
@required
// ...
}
以下为对话:
我:这不是修饰器吗(因为之前看到过@这个东西)老哥:还不错嘛,知道是修饰器,那你知道这一段想表达什么意思吗
我:这是路由?(一脸懵逼,但是看到了/user和post还有/login,心里想难道这是路由)
老哥:稳!
我:震惊了,还能够这样写路由。不行,回去我要好好看看这个破@
由此开始了修饰器的学习~~~
嘤嘤嘤~~
初识Decorator
首先说明,修饰器在JavaScript中还处于提议阶段,目前还不能够被大部分环境支持,并且之后还有可能会改变。如果想要使用该特性请用Babel进行转码或者使用JavaScript的超集TypeScript
在ES6中增加了类的相关定义和操作(比如class和extends),这样方便了我们单个类的操作,但是当我们想要在多个不同类之间共享、复用一些方法的时候,会发现变得不那么优雅,所以decorator被提了出来。
小小的demo:以@作为标识符,既可以作用于类,也可以作用于类属性
@decorator
class Cat {}
class Dog {
@decorator
run() {}
}
修饰器的使用
既然decorator与类相关,我们先了解一下类(这里只是简单介绍,想要详细了解的,请自行查阅相关资料,推荐阮一峰的ES6入门)
class Cat {
constructor(name) {
this.name = name
}
say () {
console.log("miao ~")
}
}
这其实是一个语法糖,具体的实现是通过Object.defineProperty()
方法来操作的,该方法语法如下:
Object.defineProperty(obj, prop, descriptor)
-> obj: 要在其上定义属性的对象
-> prop: 要定义或修改的属性的名称
-> descriptor:要被定义或修改的属性描述符
返回:传递给该方法的对象(即obj)
所以上面那个Cat的代码实际上在执行时是这样的:
function Cat() {}
Object.defineProperty(Cat.prototype, "say", {
value: function() {console.log("miao ~")}, // 该属性对应的值
enumerable: false, // 为true时,才能够出现在对象的枚举属性中
configurable: true, // 为true时,该属性描述符才能够被改变
writable: true // 为true时,value才能被赋值运算符改变
}) // 返回Cat.prototype
作用于类的修饰器
function isAnimal(target) {
target.isAnimal = true
return target // 返回的是传递给该函数的对象
}
@isAnimal
class Cat {
// ...
}
console.log(Cat.isAnimal) // true
上面的代码基本等同于:
Cat = isAnimal(function Cat() {})
作用于类属性的修饰器
function readonly(target, name, descriptor) {
descriptor.writable = false
return descriptor
}
class Cat {
@readonly
say () {
console.log("miao ~")
}
}
let kitty = new Cat()
kitty.say = function () {
console.log("wow ~")
}
kitty.say() // miao ~
通过将descriptor属性描述符的writable设置为false,使得say方法只读,后面对它进行的赋值操作不会生效,调用的依旧是之前的方法。
有木有觉得readonly()方法的参数似曾相识?它和上文介绍ES6中的类中提到的Object.defineProperty()
是一样的。
其实修饰器在作用于属性的时候,实际上是通过Object.defineProperty
进行扩展和封装的。所以上面的代码实际上是这样的:
let descriptor = {
value: function() {
console.log("miao ~")
},
enumerable: false,
configurable: true,
writable: true
}
descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor
Object.defineProperty(Cat.prototype, "say", descriptor)
当修饰器作用于类时,我们操作的对象是类本身,当修饰器作用于类属性时,我们操作的对象既不是类本身也不是类属性,而是它的描述符(descriptor)。
当然了,你也可以直接在target上进行扩展和封装。
function fast(target, name, descriptor) {
target.speed = 20
let run = descriptor.value()
descriptor.value = function() {
run()
console.log(`speed ${this.speed}`)
}
return descriptor
}
class Rabbit {
@fast
run() {
console.log("running~")
}
}
let bunny = new Rabbit()
bunny.run()
// running ~
// speed 20
console.log(bunny.speed) // 20
回到文章开始
让我们回到文章开始讲到的代码,它怎么阅读呢
@controller("/api/user")
export class userController {
@post("/add")
@required({
body: ["telephone", 'key1']
})
async addUser(ctx, next) {}
@get("/userlist")
async userList(ctx, next) {}
}
让我们先看@controller("/api/user")
const symbolPrefix = Symbol('prefix')
export const controller = path => target => (target.prototype[symbolPrefix] = path)
作用是将/api/user
作为prefixPath,此时target为userController {}
,在该target的原型上设置path
再接着看@post("/add")
// 以下代码省略了部分细节
...
// export const post = path => (target, name, descriptor) => {}
routerMap.set({
target: target,
...conf
}, target[name]) // name为addUser
for(let [conf, func] of routerMap) {
// conf为{target, path} 该target为userController {},path为@post()传递进来的参数
let prefixPath = conf.target[symbolPrefix] // 为 /api/user
let routePath = prefix + path // 为 /api/user/add
}
// 得到了api路径(routePath),也得到了该api路径所执行的方法(func)
// get同理
...
原理基本上都是类似的,处理修饰器传递的参数,得到自己想要的结果。
额,经历过上面的知识了解,应该能大概够理解这段代码了吧~
小结
修饰器允许你在类和方法定义的时候去注释或者修改它。修饰器是一个作用于函数的表达式,它接收三个参数 target、 name 和 descriptor , 然后可选性的返回被装饰之后的 descriptor 对象。
你也可以叠加使用,就像这样
@post("/add")
@required({
body: ["telephone", 'key1']
})
async xxx {}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。