JS 装饰器( Decorator )

JS 装饰器( Decorator )

一.概念

说起装饰器之前,先了解一下JS设计模式之装饰器模式。装饰器模式(Decorator Pattern)能够在不改变对象自身的基础上,动态的给某个对象添加额外的职责,不会影响原有的功能。这种类型的设计模式属于结构型设计模式,它是作为现有的类的一个包装

JS 装饰器(Decorator)

装饰器(Decorator)目前仍然处于第2阶段的提案中,提案地址 它可以用来装饰类、方法、属性,然后再进行功能的扩展。

之前在写 React 高阶组件(HOC) 介绍过这个装饰器模式的概念,与接下来我们要讲的,原理是一样的。

二.特点

装饰器是一种函数,写成@函数名的形式,它可以放在类和类方法的定义前面

三.使用方式

在使用装饰器的时候,我们需要引入babel模块transform-decorators-legacy,进行编译!

1.安装依赖
$ npm install @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-transform-runtime @babel/preset-env @babel/register babel-loader --save-dev
2.配置文件.babelrc

创建babel 配置文件 .babelrc进行编译

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose" : true }],
    "@babel/plugin-transform-runtime"
  ]
}
3.引入注册babel

在工程项目的入口文件中引入 @babel/register

require('@babel/register')
4.使用
//user.js
@username
export class Person {
}

function username(target) {
  target.uname='zhangsan'
}
//app.js
require('@babel/register')

const { Person }   = require('./user.js');
console.log(Person.uname);//zhangsan

三.装饰器原理

说起装饰器的原理,实际上,装饰器Decorator 是一种语法糖,依赖于Object的静态方法:Object.defineProperty 方法,之前在写Vue响应式原理的时候,粗略介绍过这个方法,这里再详细介绍下:

Object.defineProperty()

MDN:方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

Object.defineProperty(obj, property, descriptor)
  • obj:要定义属性的对象
  • property: 要定义或修改的属性的名称
  • descriptor:要定义或修改的属性描述符

返回值:返回被传递给此函数的对象obj

descriptor具有以下可选键值:

  • configurable 当且仅当该属性的configurable键值为true时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false

  • enumerable 当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性上。默认为false

  • writable 当且仅当该属性的writable键值为true时,属性的值,也就是value,才能被赋值运算符改变。默认为false

  • value该属性对应的值,可以是任何有效 的JavaScript值(数值,对象,函数等)。默认为undefined

    const newObj = Object.defineProperty({}, 'username', {
      configurable: true,//默认为false
      enumerable: true,//默认为false
      writable: true,//默认为false
      value: 'zhangsan'
    })
    
    newObj.username = 'liuqiao'
    console.log(newObj.username);//liuqiao
    

对象属性访问器 GetterSetter

  • get一个给属性提供getter的方法,如果没有getter则为undefined,当访问该属性时,该方法会被执行,默认为undefined

  • set 一个给属性提供setter的方法,如果没有setter则为undefined,当属性值修改时,触发执行该方法,该方法接受唯一参数,即该属性新的参数值,默认为undefined

    var obj = {
      name: 'liuqiao',
      age: 18,
      say: () => {
        console.log('hello world');
      }
    }
    
    Object.defineProperty(obj, 'age', {
      //是否可配置(枚举,可写,删除)
      configurable: true,
      //是否可枚举属性(遍历属性)
      enumerable: true,
      get() {
        console.log('触发getter函数');
        //需要使用中间介质变量,不能直接使用this.age,否则死循环
        return this._age
      },
      set(newValue) {
        console.log('触发setter函数');
        //需要使用中间介质变量,不能直接使用this.age,否则死循环
        this._age = newValue
      }
    })
    
    obj.age = 28
    console.log(obj.age);
    console.log(Object.getOwnPropertyDescriptor(obj,'age'));
    

    PS:一个描述符不能同时有(writable或value)和(get或set)关键字

1.装饰-类

当装饰的对象是类时,操作的就是这个类

无参数的装饰器:

@option
class Person {
  constructor(gender) {
    this.gender = gender
  }
}

function option(target) {
  target.uname = 'liuqiao'
  target.prototype.shopping = function () {
    console.log(target.uname, 'go shopping',this.gender);
  }
}

new Person("男").shopping();//liuqiao go shopping 男

装饰类时,装饰器函数接收的参数是一个目标类,然后在这个函数中我们可以对这个目标类进行添加静态属性,还可以在当前类的原型上,增加新的方法。

实际上,装饰器的这种行为,我们可以理解成这样的一种写法:

@option
class Person {

}

等同于=>

class Person {

}

//option是装饰器函数,接收Person类
Person = option(Person) || Person

有参数的装饰器:

//高阶函数
const option = (gender,age) =>(target) => {
  target.uname = 'liuqiao'
  target.gender = gender
  target.age = age
  target.prototype.shopping = function () {
    console.log(target.uname, 'go shopping', target.gender, target.age);
  }
}

@option('男', 18)
class Person {

}
new Person().shopping();

通过这里的运用,联想到了之前我写 React Redux 介绍 时,运用了一个高阶函数connect,connect的主要作用是连接React组件与React Store.并且会接收4个参数,这里我们经常是两种

export default connect(mapStateToProps, mapActionToProps)(LoyoutCom);

这里实际上就是一个高阶函数,函数柯里化之后的写法,那这样,我们今天了解了装饰器的写法,我们可以改造一下

@connect(mapStateToProps, mapActionToProps)
export default LoyoutCom extends React.Component{
    
}
2.装饰-类属性(属性,方法,get/set函数)
1. 装饰方法
/**
 * @param {是否只读,bool值} value
 * @param {类原型} target obj.prototype
 * @param {被装饰的属性或方法} property obj.method
 * @param {被修饰的属性或方法的描述对象} descriptor  
 * descriptor:{ value: f, enumarable: false, writable: true, configurable: true }
 */
const readonly = (value) => (target, property, descriptor) => {
  //设置当前属性或方法是否只读
  descriptor.writable = !value
}

class Person {
  @readonly(true)
  getName() {
    console.log('zhangsan');
  }
}

let person=new Person()
//重新赋值,报错,设置了只读
//ERROR:Cannot assign to read only property 'getName' of object '#<Person>'

person.getName=()=>{
  console.log('liuqiao');
};
person.getName()

当装饰方法时,装饰器函数一共可以接受三个参数,原理就是Object.defineProperty(target, property, descriptor),可以把装饰器就是这个函数的语法糖,但是与装饰类不同的是,这里的第一个参数target实际上是类的原型对象protottype,装饰器的本意是要装饰类的实例,但是此时实例没有生成,只能去装饰原型。不同于类的装饰,类的装饰target参数指的是类本身,第二个参数是要装饰的属性名,第三个参数是该属性的描述对象。

再来个我们经常使用到的实例,我们项目中,经常会记录操作日志和记录

const loggor = (type, desc, level, operator) => (target, prop, descriptor) => {
  var oldValue = descriptor.value;

  descriptor.value = function () {
    console.log(`调用方法名:${prop},相关参数:${arguments}`);
    return oldValue.apply(this, arguments);
  };

  console.log(`日志类型:${type},日志描述:${desc},日志级别:${level},操作人:${operator}`);
  return descriptor;

}

class Person {
  @loggor('info', '获取用户名', '1', 'admin')
  getName(name) {
    console.log(name);
  }
}

let person = new Person()
person.getName('zhangsan')

上述@logger装饰器的作用就是在执行每个操作之前,记录操作的日志。这个装饰器还可以更通用,这样写法,在代码层面来说,还是很直观的!并且还有着代码注释的作用,能一眼就看清楚,这里干了什么

2. 装饰类属性(字段)
//验证数字正则方法
const vality = (obj) => {
  var reg = /^\d+(\.\d+)?$/;
  if (reg.test(obj)) {
    return true;
  }
  return false;
}

//创建一个验证属性只能为数字的装饰器
const IsNumber = () => (target, name, descriptor) => {
  //这里target指向当前装饰字段所在类的原型,也就是User.prototype
  console.log(target,name);
  //可以获取实例化的时候此属性的默认值
  let initializer = descriptor.initializer && descriptor.initializer.call(this)
  //返回一个新的描述对象,也撸修改 descriptor 
  return {
    enumerable: true,
    configurable: true,
    get: function () {
      return initializer;
    },
    set: function (value) {
      // 在此对传入的 value 的值做各种检查
      if (!vality(value)) {
        console.error('必须为数值类型');
      } else {
        initializer = value;
      }
    }
  }
}

class User {
  @IsNumber()
  age=1
}

const user = new User()
user.age = '123a'
console.log(user.age);//null

上面是一个简单版本的@IsNumber的装饰器,此装饰器装饰的是类属性中的字段,这里的target指向,它和装饰方法一样,也是指向的当前类的原型对象protottype,所以装饰类属性时,target的指向都是一样的。

3.装饰getter和setter函数

我们知道,在ES6的class中,在 的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class User{
  get name(){
    return 'liuqiao'
  }
  set name(value){
    console.log('value:'+value)
  }
}

const user=new User()
console.log(user.name)//liuqiao
user.name='zhangsan' //value:zhangsan

这个呢是class的语法,现在呢,我们要使用装饰器,将装饰器使用在getter和setter函数上

getter函数上的使用:

//取值时,数据加单位
const Unit = (value) => (target, property, descriptor) => {
  return {
    ...descriptor,
    get() {
      return this.productName + value
    }
  }
}

class User {
  productName = 'iphone13'

  @Unit('香')
  get name() {
    return this.productName
  }
}

const user = new User()
console.log(user.name) //iphone13香

setter函数上的使用:

//前缀标识
const Prefix = (prefixValue) => (target, property, descriptor) => {
  return {
    ...descriptor,
    set() {
      this.url = prefixValue + this.url
    }
  }
}

class User {
  url = 'apis/user'

  get fullURL() {
    return this.url
  }

  @Prefix('http://')
  set fullURL(value) {
    console.log(value, 1111);
  }
}

const user = new User()
user.fullURL=''
console.log(user.fullURL) //http://apis/user

上述对getter函数和setter函数进行装饰器的操作,其实就实现了,当取值或赋值时,进行了一个额外的装饰,实际项目中,这种场景很多

3.多个装饰器执行顺序
//前缀标识
const Prefix = (prefixValue) => (target, property, descriptor) => {
  console.log(prefixValue);
}

class User {
  url = '/apis/user'

  @Prefix('www.baidu.com')
  @Prefix('127.0.0.1')
  getUrl(){

  }
}

const user = new User()
//127.0.0.1
//www.baidu.com

从上面代码中,可以看到执行结果,所以:当同一个函数存在多个装饰器时,解析器解析时会从外到内解析,但是执行时,是从里到外执行的

4.装饰器不能用于函数

当我们想要将装饰器用于普通函数,是不可取的,装饰器只能用于类和类方法或属性,但是不能装饰普通函数,因为普通函数存在声明提升!

阮一峰大神的例子看看,更加通俗易懂

var counter = 0;
var add = function () {
  counter++;
};
@add
function foo() {
}

//解析器分析
1. var counter,add
2. @add
   function foo() {
   }
3. counter=0
4. add=function () {
    counter++;
   };

下面一个例子,也能非常清晰的知道

var readOnly = require("some-decorator");

@readOnly
function foo() {
}

实际上解析
1.var readOnly
2.@readOnly
  function foo() {
  }
3.readOnly=require("some-decorator");

所以呢,根据以上2个例子,可以知道,因为函数会存在声明提升,使得装饰器不能用于函数,在class类中是没有声明提升的,所以不会有这方面的问题。

参考链接

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
是的,JavaScript引入了装饰器的概念,可以使用装饰器来修改类和类的成员。装饰器是一个函数,它可以接收一个类或类的成员作为参数,并且可以返回一个新的类或修改后的成员。 以下是一个使用装饰器修改类的示例: ``` function classDecorator(target) { // 在类名前后添加一些字符串 target.className = `decorated_${target.name}_class`; return target; } @classDecorator class MyClass { // ... } console.log(MyClass.className); // 输出为 "decorated_MyClass_class" ``` 在上面的例子中,`classDecorator`是一个装饰器函数,它接收一个类作为参数,并且在类名前后添加一些字符串。在类定义前面加上 `@classDecorator`,就可以使用装饰器来修改类。 以下是一个使用装饰器修改类成员的示例: ``` function methodDecorator(target, key, descriptor) { // 保存原始方法 const originalMethod = descriptor.value; // 修改方法 descriptor.value = function(...args) { console.log(`Method ${key} called with arguments: ${args.join(', ')}`); return originalMethod.apply(this, args); }; return descriptor; } class MyClass { @methodDecorator myMethod(x, y) { return x + y; } } const obj = new MyClass(); obj.myMethod(1, 2); // 输出 "Method myMethod called with arguments: 1, 2" console.log(obj.myMethod(1, 2)); // 输出 3 ``` 在上面的例子中,`methodDecorator`是一个装饰器函数,它接收三个参数:类的原型对象(即类的成员所在的对象)、成员名和成员的描述符。在这个例子中,`methodDecorator`修改了类的一个方法,添加了一些额外的日志输出。在方法定义前面加上 `@methodDecorator`,就可以使用装饰器来修改方法。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值