装饰者模式的作用
为类或方法增添特性或职责
用简单的例子说明,假如需要为一个类或普通函数添加功能,我们可以选择
- 直接修改代码
- 用一个函数再嵌套一层,相当于再包装一下,比如下面的例子
- 如果是类,可以通过派生子类进行拓展
function f1 (name) { console.log(name) }
// 通过b函数为a函数添加功能,进行包装
function f2() {
return (name) => {
f1(name);
console.log('f1 done'); // 这里添加的功能,就是打印'done'
}
}
// 调用b函数对a函数添加功能
const f3 = f2();
// 使用增强后的函数
f3('zhangsan');
具有装饰作用的,但不具备通用性,一般不认为是装饰器。比如上面的b函数就不算是装饰器。
假如把f2函数再抽象一下使得它可以公用,为任意或特定某些函数增强功能,它就变成了装饰器。如下:
function f2(fn) {
return () => {
fn.apply(this, arguments);
console.log(`${fn.name} done`); // 这里添加的功能,就是打印'done'
}
}
const test1 = (name) => { console.log('name:', name) }
const test2 = () => {}
f2(test1)('zhangsan');
f2(test2)();
如果使用非装饰器的其他的办法(比如直接改源码),可能会存在功能耦合太高、不灵活、不方便测试等弊端
装饰者模式的特点
- 可以嵌套装饰,装饰后,可以再次对其装饰
- 透明,使用者自行选择使用哪些装饰,很容易了解增强了什么功能,并且可以按之前方式继续使用装饰后的内容
- 动态,增强功能不是固定的,不是写死的,使用者可以自由的在合适的时机对函数进行合适的装饰
- 装饰者可以直接改变被装饰者的内容,也可以返回一个新的东西替代被装饰者
装饰者模式的应用
装饰器的应用例子:处理异常、控制函数的执行时机(防抖节流)、获取函数的执行时长等等。
处理异常
这里使用function而不是箭头函数,并且需要使用apply来调用fn,是因为参数fn如果是普通函数的话需要对其改变this指向。将fn的this指向始终设为装饰器的this并不总是对的,这里只是说明存在this这个问题。
// 定义一个错误捕获装饰器
function errorDealDecorator(fn) {
return function() {
try {
fn.apply(this, arguments);
} catch (error) {
// Do something
console.warn('errorDecorator: ', error);
// 将错误储存到localStorage
localStorage.setItem(`[${fn.name}]functionError`, JSON.stringify(error.message));
}
};
};
// 下方两个函数是被装饰对象
const o1 = () => {
throw new Error('o1 error')
};
function o2(name1, name2) {
console.log('o2:::', `${name1}、${name2}`);
};
// 对上面函数进行装饰,得到新的函数
const f1 = errorDealDecorator(o1);
const f2 = errorDealDecorator(o2);
// 使用装饰后的得到函数
f1();
f2('zhangsan', 'lisi');
EcmaScript装饰器提案
基本介绍
(tc39 是EcmaScript里面的一个专门管理 EcmaScript 提案与标准的一个小组 )
目前在Stage: 3阶段,还没有成为EcmaScript标准。
根据提案内容,可以应用于以下场景
- 类(class)
- 类属性(公共、私有、静态)
- 类方法(公共、私有、静态)
- 类访问器(公共、私有、静态)
该提案同时还增加了新的class 元素: 自动访问器 (Class auto accessors)。如下,这里暂不细究。
class C {
accessor x = 1; // 自动访问器前面有accessor关键字
}
// 相当于是:
class C {
#x = 1;
get x() {
return this.#x;
}
set x(val) {
this.#x = val;
}
}
装饰器的类型声明
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
从类型声明可以看出:
- 装饰器的参数接收一个被装饰对象和一个context对象作为参数,返回一个新的东西(取决于被装饰对象的类型)或不返回任何东西
- 装饰器的参数和返回都是固定的,不能为其增添额外参数或改变参数顺序。但是可以再套一层,请看后面的例子。
context执行上下文对象(可以先跳过这块内容,回过头来再看)
装饰器都有一个名叫context参数,从名字可以看出,它是一个执行上下文对象,里面有一些被装饰者的的信息。
- kind:可以理解为是被装饰的类型,它可能是这些值中的一个:'class'(类)、'method'(方法)、'getter'(访问器属性)、'setter'(访问器属性)、'field'(属性)、'accessor'(访问器属性,被装饰的是自动访问器才是这个值)
- name:被装饰对象的名称,比如被装饰对象是class,那他就是class的名称,如果被装饰对象是属性,那它的值是属性名称
- access:一个对象,里面是被装饰对象对应的方法,比如被装饰的对象是属性,那对象里面有get和set方法
- static:被装饰对象是否为静态类属性
- private:被装饰对象是否为私有类属性
- addInitializer:注册初始化钩子的函数,参数是一个函数。执行addInitializer函数,目的是把参数(函数)放到一个池子里面,然后在初始化后池子里面的函数会被执行。比如对一个class进行装饰,装饰完成后(装饰器执行完毕后),池子里面的函数会被执行。不同的被装饰对象执行时机略有差异。
类装饰器
类型声明
type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;
简单的例子
// 定义一个类装饰器
const component = (targetClass, context) => {
targetClass.name = 'toast'; // 给类增加一个静态属性
targetClass.prototype.clear = function() {}; // 给原型添加一个方法
};
// 使用类装饰器
@component
class Toast {}
// 使用效果相当于下面这样
// class Toast {}
// Toast = component(Toast, context);
// 验证
console.log(Toast.name);
console.log((new Toast()).clear);
装饰器可以封装到一个工厂函数里面,这样就能给装饰器传递额外的参数
// 定义一个类装饰器工厂函数(高阶函数)
const component = ({ name }) => {
// 在这个地方,还可以进行很多的处理,比如根据参数,返回不同的装饰器
// 严格来说,返回的东西才是装饰器
return function(targetClass, context) {
targetClass.name = name; // 给类增加一个静态属性
targetClass.prototype.clear = function() {}; // 给原型添加一个方法
}
};
// 使用类装饰器,可以传参了
@component({ name: 'toast' })
class Toast {}
// 验证
console.log(Toast.name);
console.log((new Toast()).clear);
属性装饰器
类型声明
type ClassFieldDecorator = (value: undefined, context: {
kind: "field";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
}) => (initialValue: unknown) => unknown | void;
*注意它的返回,它只能返回一个函数,或者不返回任何东西
简单的例子
// 定义一个属性装饰器
const prop = function (value, context) {
return function(initValue) {
if (typeof initValue !== 'string') {
// 校验属性值类型
console.error(`【${content.name}】期望类型是字符串,得到值的类型却是${typeof initValue}`);
}
return initValue;
}
}
// 使用属性装饰器
class Toast {
@prop value = 1;
}
// 使用效果相当于下面这样:
// class Toast {
// value = prop.call(this, 1);
// }
// 验证
console.log(new Toast());
跟前面class装饰器一样,也可以再套一层,通过参数进行更多类型的校验。这里就不详细举例了。
// 尝试实现一个装饰器,使得可以像下面这样使用
class Toast {
@prop({ type: String }) value = 1;
}
类方法装饰器
type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
// 定义一个方法装饰器
const finishTime = function (value, context) {
return function (...args) {
// 为目标函数添加一个打印执行时长的功能
console.time(`${context.name} finish time`);
const ret = value.call(this, ...args);
console.timeEnd(`${context.name} finish time`);
return ret;
};
}
// 使用方法装饰器
class Toast {
@finishTime
dealData(data1, data2) {
console.log(data1, data2);
return data1 + data2;
}
// 嵌套使用装饰器,在提案中没看到关于这个的描述,暂不细究
// @finishTime1
// @finishTime2
// test(data1) {}
}
// 使用效果大致相当于下面这样:
// class Toast {
// dealData(data1, data2) {}
// }
// Toast.prototype.dealData = finishTime(Toast.prototype.dealData, context) ?? Toast.prototype.dealData;
// 验证
console.log((new Toast()).dealData(1, 2));
跟前面的装饰器一样,也可以再套一层,通过参数做更多的功能。这里就不详细举例了。
// 尝试实现一个装饰器,使得可以像下面这样使用
class Toast {
// 如果函数dealData在200毫秒内执行完毕,则打印一个信息
@finishTime(200)
dealData(data1, data2) {
console.log(data1, data2);
return data1 + data2;
}
}
类访问器装饰器
类型声明
type ClassGetterDecorator = (value: Function, context: {
kind: "getter";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
type ClassSetterDecorator = (value: Function, context: {
kind: "setter";
name: string | symbol;
access: { set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
类访问器装饰器与类方法装饰器是很相似的,因为访问器的本质也是函数。
所以可以用上面的类方法装饰器例子改一下,支持访问器属性。
// 定义一个装饰器
const finishTime = function (value, context) {
// 判断是否是类方法或访问器
if (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter') {
return function (...args) {
// 为目标函数添加一个打印执行时长的功能
console.time(`${context.name} finish time`);
const ret = value.call(this, ...args);
console.timeEnd(`${context.name} finish time`);
return ret;
}
}
}
// 使用装饰器
class Toast {
total = 0;
@finishTime
set count(data1) {
console.log(data1);
this.total = data1 * 2;
}
}
// 验证
console.log((new Toast()).count = 100);
自动访问器装饰器
这里直接搬运提案中的例子,因为还没完全明白自动访问器的作用,就先不折腾了
function logged(value, { kind, name }) {
if (kind === "accessor") {
let { get, set } = value;
return {
get() {
console.log(`getting ${name}`);
return get.call(this);
},
set(val) {
console.log(`setting ${name} to ${val}`);
return set.call(this, val);
},
init(initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
}
};
}
// ...
}
class C {
@logged accessor x = 1;
}
let c = new C();
// initializing x with value 1
c.x;
// getting x
c.x = 123;
// setting x to 123
其他
差异
最新的ES提案、Babel支持的装饰器特性、ts支持的装饰器特性、浏览器支持的特性(如果有的话),是存在差异的。ts提案在成为标准之前,还可能会被多次修改甚至删除。目前tabel、ts、各浏览器似乎都还没有完整实现ES最新的提案,所以上面的例子可能也没办法实际运行起来。
Babel支持的装饰器特性,是根据旧提案实现的。
Typescript在很久之前特地为装饰器做了支持,那时候ES提案还处于 Stage-0阶段。
而TypeScript 原则上只会对 Stage-3 以上的语言提案提供支持,可以说,当时ts因为某些原因特地加了装饰器特性,这也进一步导致差异的产生。 ts装饰器文档:www.tslang.cn/docs/handbo…
但是并不会对学习和使用装饰者模式产生很大的干扰,因为不管何种形式,它的核心思想和使用方式,并没有很大区别。