decorator 装饰器
前置知识
面向切面编程(AOP)
AOP 即面向切面编程(Aspect Oriented Programming)
类似于“洋葱模型”,AOP 主要意图是将日志记录,性能统计,安全控制,异常处理等代码从业务逻辑代码种抽离出来,将它们独立到非核心业务逻辑的方法中,进而达到改变这些功能行为的时候不会影响核心业务逻辑的代码。
简而言之:就是“优雅”的把“辅助功能逻辑”从“核心业务逻辑”中分离、解耦出来。
控制反转(IoC)
IoC 即 控制反转(Inversion of Control) 是解耦的一种设计模式。
在传统的程序设计中,我们的代码直接控制所有的流程和对象的创建。而采用 IoC 后,这种控制权被反转了,意味着对象的创建和流程的控制不是由我们的代码直接管理,而是交给外部的容器或框架来处理。
AOP 可以通过 IoC 容器来实现。
依赖注入(DI)
DI 即 依赖注入(Dependency Injection),是 IoC 的一种具体实现。
依赖注入允许我们的代码在运行时接收它的依赖项,而不是硬编码它们,从而提高了代码的模块化和可测试性。
属性描述符
Object.defineProperty(obj, props, descriptor) 的作用是直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。该方法接收三个参数:
要定义属性的对象(obj)
要定义或修改的属性名或
Symbol(props)要定义或修改的属性描述符(descriptor)
属性描述符有两种主要形式
数据描述符:是一个具有值的属性(value),该值可以是可写的,也可以是不可写的(writable)
存取描述符:由 getter 函数和 setter 函数所描述的属性。
一个属性描述符只能是这两者种的其中一种,不能同时是两者。
共享的属性描述符键值
configurable:属性是否可以被删除或者是否可以再次修改其属性描述符,默认值为false如果 configurable 设置为 true,则该属性的描述符可以被改变,也可以从所属对象上删除该属性。
如果 configurable 设置为 false,则除了直接修改属性的值和修改其 writable 属性为 false 之外,不能做其他修改。也就是说,一旦属性被设置为不可配置(configurable: false),就不能再将它变回可配置的了。
enumerable:属性是否会出现在对象的枚举属性种,默认值为false
数据描述符特有键值
value:该属性对应的值,默认值为undefinedwritable:属性是否可以被修改,默认值是false
存取描述符特有键值
get:一个给属性提供 getter 的方法。当访问该属性时,会调用此方法,并返回其返回值。默认为
undefinedset:一个给属性提供 setter 的方法。当属性值被修改时,会调用此方法,该方法将接收唯一的参数,即该属性的新值。默认为
undefined
定义
装饰器模式
装饰器模式(Decorator Pattern)是一种抽象的设计模式,核心思想是在不修改原有代码情况下,对功能进行扩展
遵循的设计模式原则
单一职责原则
开闭原则
装饰器
装饰器(Decorator)是一种特殊的装饰类函数,是一种对装饰器模式理念的具体实现。
它接受一个函数或类作为参数,并返回一个已经被修改或增强功能的函数或类的版本
装饰器工厂函数
装饰器工厂函数是返回一个装饰器的函数。这使得装饰器可以接受参数,因此可以在不同的场景下提供更多的灵活性。装饰器工厂函数先执行,其返回值(一个装饰器函数)随后应用于目标函数或类。
装饰器语法
@decorator 装饰器语法是一种便捷的语法糖,通过 @ 来引用,需要编译后才能进行。
装饰器用法
装饰器语法@decorator,不过目前还处于第 2 阶段提案中,使用它之前需要使用 TypeScript 或 Babel 模块编译成 ES5 或 ES6。
类装饰器
语法
参数:接受一个参数
target:类的构造器
返回值:如果类装饰器返回了一个值,她将会被用来代替原有的类构造器的声明
适用场景
给当前类添加一些属性和方法
继承当前类,并进行扩展
举例
可以添加一个 addToJsonString 方法给所有的类来新增一个 toString 方法
方法装饰器
语法
参数
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
propertyKey: 属性的名称
descriptor: 属性的描述器
返回值:如果返回了值,它会被用于替代属性的描述器。
使用场景
方法装饰器可以实现与 Before / After 钩子 相关的场景功能。
举例
属性装饰器
语法
语法跟方法装饰器类似,只不过不要随意修改 decorator 的 value,因为属性装饰器会在代码运行前生效,所以修改了 value 可能不会生效。
使用场景
修改属性的描述符
举例
访问器装饰器
语法
语法跟方法装饰器类似,唯一的区别在于第三个参数 descriptor 描述器中有的 key 不同:
方法装饰器的描述符的 key 为:
value
writable
enumerable
configurable
访问器装饰器的描述符的 key 为:
get
set
enumerable
configurable
使用场景
可以在某个属性赋值的时候做加一层代理
举例
参数装饰器(TypeScript 实现)
语法
参数:接收三个参数
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
methedKey: 方法的名称,注意!是方法的名称,而不是参数的名称
parameterIndex: 参数在方法中所处的位置的下标
返回:返回的值将会被忽略
使用场景
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
举例(需要 TypeScript 编译)
装饰器的实现原理
给 run 方法增加耗时计算
硬编码
硬编码存在的问题:
理解成本高:统计耗时的相关代码与函数本身逻辑并无关系,对函数结构造成了破坏性的修改,影响到了对原函数本身的理解
维护成本高:如果后期还有更多类似的函数需要添加统计耗时的代码,在每个函数中都添加这样的代码非常低效,也大大提高了维护成本
手动实现装饰器模式
核心思路
Step1 备份原来类构造器 (Class.prototype) 的属性描述符 (Descriptor)
利用 Object.getOwnPropertyDescriptor 获取
Step2 编写装饰器函数逻辑代码
利用执行原函数前后钩子,添加耗时统计逻辑
Step3 用装饰器函数覆盖原来属性描述符的 value
利用 Object.defineProperty 代理
Step4 手动执行装饰器函数,装饰 Class(类) 指定属性
从而实现在不修改原代码的前提下,执行额外逻辑代码
代码实现
手动实现的装饰器需要优化
是否可以让装饰器更加关注业务逻辑?Step 1 和 Step3 是通用逻辑,每个装饰器都需要实现,可以抽离复用
是否可以让装饰器写法更简单?纯函数实现的装饰器,每装饰一个属性都要手动执行装饰器函数,详见 Step4 步骤。
针对上述优化点,装饰器草案中提出了装饰器语法糖,也就是 @Decorator ,只需要在想使用的装饰器前加上@符号,装饰器就会被应用到目标上。
装饰器语法糖
装饰器的执行顺序
同种装饰器组合顺序:洋葱模型
如果同一个方法有多个装饰器,其执行顺序是怎样的?
答案:以方法装饰器为例,同种装饰器组合后,其顺序会像剥洋葱一样,先从外到内进入,然后由内向外执行。和 Koa 的中间件顺序类似。
不同类型装饰器顺序
参数装饰器先执行,按照其被应用到的方法参数从最后一个到第一个依次执行。
方法装饰器和属性装饰器接着执行,按照它们声明的顺序从上到下执行。
类装饰器最后执行,如果有多个类装饰器,它们的执行顺序是从下到上(即与声明顺序相反)。
值得注意的是,装饰器工厂函数的调用顺序和装饰器的执行顺序是不同的。工厂函数是在代码解析阶段就被调用的,而装饰器本身的执行是在所有工厂函数调用之后,按照上述规则进行的。
装饰器的优缺点
优点
在不修改原有代码情况下,对功能进行扩展。也就是对扩展开放,对修改关闭。
优雅地把“辅助性功能逻辑”从“业务逻辑”中分离,解耦出来。(AOP 面向切面编程的设计理念)
装饰类和被装饰类可以独立发展,不会相互耦合
装饰模式是 Class 继承的一个替代模式,可以理解成组合
缺点
但是糖再好吃,也不要吃太多,容易坏牙齿的,滥用过多装饰器会导致很多问题:
理解成本:过多带业务功能的装饰器会使代码本身逻辑变得扑朔迷离
调试成本:装饰器层次增多,会增加调试成本,很难追溯到一个 Bug 是在哪一层包装导致的
注意事项
装饰器的功能逻辑代码一定是辅助性的
比如日志记录,性能统计等,这样才符合 AOP 面向切面编程的思想,如果把过多的业务逻辑写在了装饰器上,效果会适得其反。
装饰器语法尚未定案以及未被纳入 ES 标准,标准化的过程还需要很长时间
阅读
Last updated