Better

Ethan的博客,欢迎访问交流

ES7 Decorator 和 AOP

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)
  }
}

更多



留言