学习《JavaScript设计模式与开发实践》- 装饰者模式

335 阅读5分钟

一个例子

假如我们现在想设计一个可以配置自行车的游戏,自行车由玩家自行配置,包括有没有前面的框,后面的座椅,照明灯,换挡器等。

常规情况下,我们应该把每一种零件配置都定义为类,然后加装一个配件,就去继承它。但是如果这样的话,我们需要定义非常多的类,并且会出现很多继承的情况,让我们的代码变得非常冗余。

我们希望用另一种更简单的方法,动态的去给自行车的类增加功能。

装饰者模式的作用

装饰者(decorator)模式是一种可以给对象动态地增加职责的方式,式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。

跟继承相比,装饰者模式显得更加的轻量。

使用装饰者模式实现自行车配置

首先我们实现自行车的类

function Bicycle() {
	//setting......
}
Bicycle.prototype.func = function(){
	console.log("自行车的功能");
}

之后我们将不同的功能设计为不同的包装类,分别是车灯,车框,车后座,每一个包装类都增强了原始类的功能函数,以此达到继承的作用。

function LightingBicycle(bicycle) {
	this.bicycle = bicycle;
}
LightingBicycle.prototype.func = function(){
	this.bicycle.func();
	console.log("发光");
}
function FrameBicycle(bicycle) {
	this.bicycle = bicycle;
}
FrameBicycle.prototype.func = function(){
	this.bicycle.func();
	console.log("放置");
}
function SeatBicycle(bicycle) {
	this.bicycle = bicycle;
}
SeatBicycle.prototype.func = function(){
	this.bicycle.func();
	console.log("搭载");
}

最后简单测试一下

var bicycle = new Bicycle();
bicycle = new LightingBicycle(bicycle);
bicycle = new FrameBicycle(bicycle);
bicycle = new SeatBicycle(bicycle);
bicycle.func();
//打印
//自行车的功能
//发光
//放置
//搭载

以上我们简单的实现了对类的包装,并且每一个新功能又是一个包装类,我们可以按需求来进行增强,非常的方便。除了装饰类,我们也可以装饰函数.

对函数的装饰

当我们想增强函数的功能的时候,我们通常会对函数进行直接的修改,但是如果函数非常的杂乱庞大,我们可以考虑使用装饰来增强它。

继续使用上一个自行车的例子,如果我们是单纯的想去额外增强一下func的功能,那我们可以先使用另外一个方法引用它,然后重写它

function Bicycle() {
	//setting......
}
Bicycle.prototype.func = function(){
	console.log("自行车的功能");
}
var _func = Bicycle.prototype.func;
Bicycle.prototype.func = function(){
	_func();
	console.log("新增的功能");
}

因为中间使用了其他的变量引用函数,所以我们在这里需要考虑this的指向。

比如我们想增强document.getElementById方法

var _getElementById = document.getElementById; 
document.getElementById = function( id ){ 
 alert (1); 
 return _getElementById( id ); // (1) 
} 

这段代码执行之后,会抛出错误,因为在使用getElementById这个方法的时候,其中的this是指向document的,当我们使用一个新的变量指向它的时候,实际上这个时候的this已经指向window了,所以要正确使用的话,我们需要在增强方法的调用中绑定一下this。

var _getElementById = document.getElementById; 
 document.getElementById = function(){ 
 alert (1); 
 return _getElementById.apply( document, arguments ); 
 } 

ES7提供的装饰器

上面的装饰器方法在现在其实已经很少使用了,因为ES7提供了很方便强大的装饰器方法。

开启装饰器的配置,参考 blog.csdn.net/weixin_4276…

类装饰器

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。

基本上,装饰器的行为就是下面这样。

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。

如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

方法装饰器

相关知识:Object.defineProperty

装饰器不仅可以装饰类,还可以装饰类的属性。

Object.defineProperty()在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法: Object.defineProperty(obj, prop, descriptor)

  • obj:操作的对象
  • prop:被定义或者修改的属性名称
  • descriptor:将被定义或修改的属性描述符
  • 返回值:被传递给函数的对象
class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

上面代码中,装饰器 readonly 用来装饰“类”的name方法。

装饰器函数 readonly 一共可以接受三个参数。

  • 第一个参数是 类的原型对象,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);
  • 第二个参数是 所要装饰的属性名
  • 第三个参数是 该属性的描述对象
function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);

函数的方法装饰器可以做许多的事情,可以阅读一下core-decorators.js,是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器

注意:装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

参考:segmentfault.com/a/119000001…

最后

装饰者模式是为已有功能动态地添加更多功能的一种方式,把每个要装饰的功能放在单独的函数里,然后用该函数包装所要装饰的已有函数对象,因此,当需要执行特殊行为的时候,调用代码就可以根据需要有选择地、按顺序地使用装饰功能来包装对象。优点是把类(函数)的核心职责和装饰功能区分开了,减少了很多不必要的代码。