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
对象属性访问器 Getter
和Setter
-
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中,在类
的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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类中是没有声明提升的,所以不会有这方面的问题。