ES7 为我们提供了很多强大的方法,我们目前最常用的应该是 async/await,今天我们另一个新特性-decorator。
背景知识
JavaScript 这种动态语言天生有在运行时对属性进行动态替换或修改的能力,这就叫做猴子补丁(Monkey patch)。
在JavaScript 的世界里 Decorator 和 AOP 编程都是基于这种可以动态修改原有属性的能力实现的。
高阶函数(Higher Order Function):高阶函数接受一个或多个函数,并返回一个函数。我们经常用到的高阶函数有:once, debounce, memoize, fluent等。
基本介绍
Decorators的作用,对一个类或者其属性方法进行装饰,让它功能更加强大。语法非常简单,就是在一个类或者其属性方法前面加上@decorator,decorator 指的是装饰器的名称。装饰器本身是一个函数,因为在函数内部,我们可以进行任意的操作从而对其进行增强。
在我们日常开发过程中,Decorator 模式常见的使用场景有: logging, Add Burying Point, Apply Formatting, Apply Permission Checks, Form validating, Block Overriding of methods, Timing Functions, Rate-Limiting 和任何你不想放在核心业务代码中的行为。
环境构建
现在语法还不能直接被浏览器支持,如果我们想实践的话,就需要使用 babel 进行语法转换,这里我们使用 webpack 进行简单的环境搭建。
首先装饰器的核心插件是:babel-plugin-transform-decorators-legacy,我们还需要安装 babel-loader,执行如下命令:
npm i --save-dev babel-core babel-loader babel-plugin-transform-decorators-legacy html-webpack-plugin webpack
使用npm init新建项目后,在根目录创建webpack.config.js,内容如下:
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: path.join(__dirname,'index.js'),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].[chunkhash:8].js'
},
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options: {
plugins: ['transform-decorators-legacy']
}
},
exclude: path.join(__dirname, 'node_modules'),
}
]
},
plugins:[
new htmlWebpackPlugin({
template: './index.html'
})
]
}
在 package.json 中添加脚本:"build": "webpack",此时我们运行 npm run build 便可以编译代码了。
使用尝试
假设我们有一只狗,我们给他添加一个属性color,初始代码如下:
class Dog{
bark(){
console.log(`汪汪汪,我是${this.color}狗`);
}
}
我们通过bark方法,说出狗的颜色,可是我们此时没有color属性啊,这就是想要演示 Decorator 的地方,代码修改如下:
@addBlack
class Dog{
bark(){
console.log(`汪汪汪,我是${this.color}狗`);
}
}
function addBlack(target,key,descriptor){
target.prototype.color = 'black';
}
这样bark方法就会说自己是黑狗啦,可是我们不能有红狗的时候就加一个addRed装饰器吧,很显然,我们需要知道怎么传参。但这一点内容和装饰器无关,装饰器本质是一个函数,引用装饰器时,会自动传参target,key,descriptor,这里解释后面再说。要实现传参,主要用到高阶函数,我们返回一个装饰器即可。
@addColor('red')
class Dog{
bark(){
console.log(`汪汪汪,我是${this.color}狗`);
}
}
function addColor(color){
return (target,key,descriptor) => {
target.prototype.color = 'color';
}
}
这样 bark 方法就可以愉快的说出自己是什么颜色的狗了,但是如果外部引用不小心把 bark 方法重写了,那就不正常了,所以我们可以指定 bark 方法不可重写么。
我们知道指定元素不可重写可以需要用到Object.defineProperty函数,首先我们可以看看定义在class内部的函数,属性描述初始值是什么:
console.log(Object.getOwnPropertyDescriptor(Dog.prototype, 'bark'))
它的默认值属性,writable: true, enumerable: false, configurable: true, 可写,可配置,不可枚举。因此我们要想不可以重写,将writable设置为false即可。
Object.defineProperty(Dog.prototype, 'bark', {
value:function () {
console.log(`汪汪汪,我是${this.color}狗`);
},
writable: false
})
当我们配置后,纵然可以添加同名属性,但不会被复写,增加了安全性,但是当多个属性需要设置为不可重写时,我们总不能一个一个写吧,这时候 Decorator 就可以发挥作用啦。此时代码如下:
@addColor('red')
class Dog{
@readonly
bark(){
console.log(`汪汪汪,我是${this.color}狗`);
}
}
function addColor(color){
return (target,key,descriptor) => {
target.prototype.color = 'color';
}
}
function readonly(target,key,descriptor){
descriptor.writable = false;
return descriptor;
}
这样我们效果就达到了,装饰器其实也是利用object.defineProperty 重新定义了属性或方法。
参数详解
你可能比较感兴趣,装饰器给我们提供的三个参数target,key,descriptor的含义是什么呢?
我们分别对上述两个装饰器打印一下这三个参数,惊讶的发现他们是不同的。
直接作用在对象上的
- target:就是class,从ES5的角度而言就是构造函数本身
- key和descriptor是空
作用在对象属性上的
- target:函数原型
- key:属性名称
- descriptor:等同于Object.getOwnPropertyDescriptor的结果
AOP
一次表单提交,有正常的业务提交过程,但我们想在这个提交过程的横向加一个表单验证。或者一个正常的业务中,我们希望横向添加一些埋点功能,同时再横向添加运行时错误信息收集的功能,同时还能够验证一下是否有操作权限等,这些都是面向切面编程
如果你遇到了需要从外部增加一些行为,进而合并或修改既有行为,或者把业务逻辑代码和处理琐碎事务的代码分离开,以便能够分离复杂度等的业务场景,请一定要用好这种编程设计思想
这里顺便提到 AOP 编程,主要就是因为我们可以使用 JavaScript Decorators 来更优雅地实践 AOP 编程,这里引用下面更多链接中一个表单验证的例子帮助自己理解
var validateRules = {
expectNumber(value) {
return Object.prototype.toString.call(value) === '[object Number]'
},
maxLength(value) {
return value <= 30
}
}
function validate(value) {
return Object.keys(validateRules)
.every(key => validateRules[key](value))
}
function enableValidate(target, name, descriptor) {
const fn = descriptor.value
if (typeof fn === 'function') {
descriptor.value = function(value) {
return validate(value)
? fn.apply(this, [value])
: console.error('Form validate failed!')
}
}
return descriptor
}
class Form {
@enableValidate
send(value) {
console.log('This is send action', value)
}
}