装饰器的定义
- 装饰器是一种与类相关的语法,主要用来包装、修改类或类的方法。更多相关内容可参考:github.com/tc39/propos…
装饰器示例
- 在类上使用装饰器
@testable
class MyTestableClass {
constructor(name) {
this.name = name;
}
}
function testable(target) {
target.isTestable = true;
}
console.log(MyTestableClass.isTestable);//true
@testable
就是一个装饰器。它修改了MyTestableClass
这个类的行为,为它加上了静态属性isTestable
。其中testable
函数的参数target
是MyTestableClass
类本身。
- 在类的方法上使用装饰器
class MyTestableClass {
constructor(name) {
this.name = name;
}
@log
testSayName() {
console.log(this.name);
}
}
function testable(target) {
target.isTestable = true;
}
new MyTestableClass('nameABC').testSayName();
//调用方法testSayName前打印
//nameABC
//调用方法testSayName后打印
/**
* @param target 被装饰的属性方法
* @param name 被装饰的属性方法的名称
* @param descriptor 被装饰的属性方法的修饰符
*/
function log(target, name, descriptor) {
console.log(target, name, descriptor);
const fn = descriptor.value;
descriptor.value = function(...rest) {
console.log(`调用方法${name}前打印`);
fn.call(this, ...rest);
console.log(`调用方法${name}后打印`);
};
}
@log
也是一个装饰器。它修改了testSayName
这个类的方法的行为,在该方法执行前后分别添加了两次输出日志。其中log
函数的参数target
是MyTestableClass
类中testSayName
这个属性方法本身,name
是testSayName
这个属性方法名称,descriptor
是testSayName
这个属性方法的修饰符,包括writable
,enumerable
,configurable
等。
- 组合使用装饰器
class MyTestableClass {
constructor(name) {
this.name = name;
}
@time
@log
testSayName() {
console.log(this.name);
}
}
function testable(target) {
target.isTestable = true;
}
new MyTestableClass('nameABC').testSayName();
// time
// 调用方法testSayName前打印
// nameABC
// 调用方法testSayName后打印
// time
function log(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function(...rest) {
console.log(`调用方法${name}前打印`);
fn.call(this, ...rest);
console.log(`调用方法${name}后打印`);
};
}
function time(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function(...rest) {
console.log('time');
fn.call(this, ...rest);
console.log('time');
};
}
组合使用装饰器时,会按照装饰器离被装饰属性的远近顺序依次执行。
注意:装饰器不能用于普通函数,只能用于类及类的方法,因为存在函数提升。
装饰器用途
装饰器的一般用途可大致分为以下几类:
- 返回包裹函数
function log(target, key, descriptor) {
return function(...args) {
const confirmOperate = window.confirm('确定执行操作吗?');
if (confirmOperate) {
descriptor.value.call(this, ...args);
}
};
}
class MyTest {
age=18;
@log()
btnClick() {
fetch('/api/delete');
}
}
- 对descriptor
writable
,enumerable
等属性的修改
function readonly(target, key, descriptor) {
descriptor.writable = false;
}
class MyTest {
age=18;
@readonly
name='123';
}
const t = new MyTest();
t.age = 20;
// t.name = 'aaa'; //尝试修改时会报错
console.log(t.age, t.name);
- 对descriptor的
value
的修改
function log(myParam) {
console.log('myParam', myParam);
return function(target, key, descriptor) {
const fn = descriptor.value;
descriptor.value = fn.bind(this, myParam);
};
}
class MyTest {
constructor(age) {
this.age = age;
}
@log('something')//在ts中,此种写法称为装饰器工厂,区别于普通装饰器,装饰器工厂可传参
btnClick(a, b) {
console.log(a, b);//something lall
// fetch('/api/delete', a, b);
}
}
console.log((new MyTest(18)).btnClick('lall'));
装饰器实践
以下代码均以在vue@2.x环境为例。
- 防抖节流
function debounce(time){
let timer = null;
return function(target,name,descriptor){
const fn = descriptor.value;
descriptor.value = function(...args){
if(timer) clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this,args)
},time)
}
}
}
var app = new Vue({
el: '#app',
data: { message: 'Hello Vue!' },
mounted(){
document.body.addEventListener('scroll',this.handleScroll)
},
methods:{
@debounce(200)
handleScroll(){
console.log('scroll')
}
}
})
与传统的防抖截流函数相比较,使用装饰器可以简化调用过程。
- 操作确认
- 手动埋点
- 数据校验 不论是操作确认、手动埋点还是数据校验,其基本原理与防抖节流都是相同的,即在用户进行某项操作后的回调函数前增加特定功能的装饰器,执行一些通用的逻辑。
- 继承(mixin)
function myMixin(...fns) {
return function(target) {
fns.forEach((item) => {
console.log(item, item.length);
Reflect.ownKeys(item).forEach((i) => {
if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
const desc = Object.getOwnPropertyDescriptor(item, i);
Object.defineProperty(target, i, desc);
}
});
Reflect.ownKeys(item.prototype).forEach((i) => {
if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
const desc = Object.getOwnPropertyDescriptor(item.prototype, i);
Object.defineProperty(target.prototype, i, desc);
}
});
});
};
}
class P1 {
constructor() {}
ha() {
console.log('ha');
}
}
class P2 {
constructor(age) {
this.age = 18;
}
hi() {
console.log('hi');
}
}
@myMixin(P1, P2)
class Person {
constructor(name) {
this.name = name;
}
}
const p = new Person('me');
console.log(p.name, p.age);
console.log(p.ha());
console.log(p.hi());
这种方式实现了多重继承时的原型和静态属性的继承,单不能解决constructor中实例属性的继承和传参问题。故将原有的返回值由target改成另一个类可解决此问题。
function myMixin(...fns) {
return function(target) {
const list = [...fns, target];
class M {
constructor(...args) {
const tempArguments = args;
const that = this;
list.forEach((Item) => {
const len = Item.length;
const temp = new Item(tempArguments.splice(0, len));
Reflect.ownKeys(temp).forEach((i) => {
const desc = Object.getOwnPropertyDescriptor(temp, i);
Object.defineProperty(that, i, desc);
});
});
}
}
list.forEach((item) => {
Reflect.ownKeys(item).forEach((i) => {
if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
const desc = Object.getOwnPropertyDescriptor(item, i);
Object.defineProperty(M, i, desc);
}
});
Reflect.ownKeys(item.prototype).forEach((i) => {
if (i !== 'prototype' && i !== 'name' && i !== 'constructor') {
const desc = Object.getOwnPropertyDescriptor(item.prototype, i);
Object.defineProperty(M.prototype, i, desc);
}
});
});
return M;
};
}
class P1 {
constructor() {}
ha() {
console.log('ha');
}
}
class P2 {
constructor(age) {
this.age = age;
}
hi() {
console.log('hi');
}
}
@myMixin(P1, P2)
class Person {
constructor(name) {
this.name = name;
}
}
const p = new Person(18, 'me');
console.log(p.name, p.age);
console.log(p.ha());
console.log(p.hi());
以上基本实现了多个类的混合继承,但仍然需要注意构造函数的传参顺序及数量,父类与子类存在同名变量时,父类的属性会被子类覆盖。
拓展-装饰者模式
给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法
在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。在JavaScript中可以很方便的为某个对象添加属性或方法,最简单粗暴的就是直接改写该对象,但却违反了函数的开放-封闭原则。
开放封闭原则:核心思想是软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
Function.prototype与装饰器是实现装饰者模式的重要方案,装饰者模式常用于数据统计上报、动态修改函数参数及表单校验等场景。