table of contents

1概述

当我们想要扩展一个对象的能力时,可以采用的方案有

  • 添加原型方法
  • 修改构造函数
  • 继承
  • 装饰者模式

前面三种方法,都不可避免的会修改原有对象的代码。

而如果不修改原有对象代码的情况下,装饰者模式是很好的一种解决方案。

2案例

首先,我们有设计了几件装备,他们的信息保存在 config.js

code.ts
1
// config.js
2
export const cloth = {
3
name: '七彩炫光衣',
4
hp: 1000
5
}
6
export const weapon = {
7
name: '青龙偃月刀',
8
attack: 2000
9
}
10
export const shoes = {
11
name: '神行疾步靴',
12
speed: 300
13
}
14
export const defaultRole = {
15
hp: 100,
16
atk: 50,
17
speed: 125,
18
cloth: null,
19
weapon: null,
20
shoes: null,
21
career: null,
22
gender: null
23
}

然后创建一个基础的角色对象,添加基础的属性与方法

code.ts
1
// 基础角色类
2
// 有血条,攻击力,速度三个基础属性
3
// 以及衣服,武器,鞋子三个装备插槽
4
class Role {
5
constructor(role) {
6
this.hp = role.hp;
7
this.atk = role.atk;
8
this.speed = role.speed;
9
this.cloth = role.cloth;
10
this.weapon = role.weapon;
11
this.shoes = role.shoes;
12
}
13
run() {}
14
attack() {}
15
}

然后基于基础角色类创建职业为战士的角色类

code.ts
1
// 战士
2
class Soldier extends Role {
3
constructor(role) {
4
const r = Object.assign({}, defaultRole, role)
5
super(r)
6
this.nickname = r.nickname
7
this.gender = r.gender
8
this.career = '战士'
9
// 战士的基础血条 +20
10
if (role.hp == defaultRole.hp) {
11
this.hp = defaultRole.hp + 20
12
}
13
// 战士的基础移动速度 +5
14
if (role.speed == defaultRole.speed) {
15
this.speed = defaultRole.speed + 5
16
}
17
}
18
run() {
19
console.log('战士的奔跑动作')
20
}
21
attack() {
22
console.log('战士的攻击动作')
23
}
24
}

接下来,我们要创建装饰类。

装饰类不必知道被装饰类的存在。他是相对独立的。我们可以称之为装饰者。

装饰类的关键,是以被装饰者的实例,作为初始化参数。

装饰类可能会有许多,衣服武器鞋子等都可以各设计一个装饰类分别负责不同的行为与变化。因此我们需要设计一个基础装饰类作为各装饰类的父类,用于减少代码量。

装饰类与被装饰类的属性与方法基本保持一致,只是实现上略有差异。

code.ts
1
// 基础装饰类
2
class Decorator {
3
constructor(role) {
4
this.role = role;
5
this.hp = role.hp;
6
this.atk = role.atk;
7
this.speed = role.speed;
8
this.cloth = role.cloth;
9
this.weapon = role.weapon;
10
this.shoes = role.shoes;
11
this.career = role.career;
12
this.gender = role.gender;
13
this.nickname = role.nickname;
14
}
15
run() {
16
this.role.run()
17
}
18
attack() {
19
this.role.attack()
20
}
21
}

接下来创建衣服装饰类 ClothDecorator

衣服装饰类继承基础装饰类

衣服只会修改角色的属性,并不会修改角色的行为

code.ts
1
class ClothDecorator extends Decorator {
2
constructor(role, cloth) {
3
super(role)
4
this.cloth = cloth.name
5
this.hp += cloth.hp
6
}
7
}

类封装好了之后,我们使用一下,感受一下变化

code.ts
1
const baseInfo = {...defaultRole, nickname: 'alex', gender: 'man'}
2
// 创建一个战士角色
3
const alex = new Soldier(baseInfo)
4
alex.run()
5
alex.attack()
6
console.log(alex)
7
8
alex = new ClothDecorator(alex, cloth)
9
// 查看变化
10
console.log(alex)

我们看属性 cloth 与 hp 已经发生了变化。

除此之外,我们还需要创建武器装饰类,鞋子装饰类。

武器与鞋子的穿戴会改变角色的攻击动作与奔跑动作,因此需要做更多的修改

code.ts
1
class WeaponDecorator extends Decorator {
2
constructor(role, weapon) {
3
super(role)
4
this.weapon = weapon.name
5
this.atk += weapon.attack
6
}
7
attack() {
8
console.log('装备了武器,攻击速度变得更快了')
9
}
10
}
code.ts
1
console.log('--------装备武器-----------')
2
alex = new WeaponDecorator(alex, weapon)
3
alex.attack()
4
console.log(alex)

鞋子装饰类

code.ts
1
class ShoesDecorator extends Decorator {
2
constructor(role, shoes) {
3
super(role)
4
this.shoes = shoes.name
5
this.speed += shoes.speed
6
}
7
run() {
8
console.log('穿上了鞋子,奔跑速度变得更快了')
9
}
10
}
code.ts
1
console.log('--------装备鞋子-----------')
2
alex = new ShoesDecorator(alex, shoes)
3
alex.run()
4
console.log(alex)

除此之外,我们玩游戏时,还知道每个角色都会在某些情况下获得不同的 buff,例如大龙 buff,小龙 buff,红蓝 buff 等,这些buff 有的会更改角色属性,例如 cd 更短,攻击更高,有的会更改攻击特性,例如红 buff 会持续掉血,减速等。

我们可以思考一下如何实现这些功能。

3decorator

INFO

默认情况下并不支持装饰器语法,因此,在学习该语法之前,你需要找到一个支持该语法的开发环境 如何在构建环境中支持 decorator

ES7 中提供了一个快捷的语法用来解决与装饰者模式一样的问题。这就是装饰器语法 decorator。

在学习装饰器语法之前,需要先温习一下 ES5 的一些基础知识。

假设有对象如下:(便于理解)

code.ts
1
var person = {
2
name: 'TOM'
3
}

对象中的每个属性都有一个特性值来描述这个属性的特点,他们分别是:

  • configurable: 属性是否能被 delete 删除,当值为false时,其他特性值也不能被改变,默认值为true
  • enumerable: 属性是否能被枚举,也就是是否能被 for in 循环遍历。默认为 true
  • writable: 是否能修改属性值。默认为 true
  • value:具体的属性值是多少,默认为 undefined
  • get:当我们通过person.name访问 name 的属性值时,get 将被调用。该方法可以自定义返回的具体值是多少。get 默认值为 undefined
  • set:当我们通过person.name = 'Jake'设置 name 属性值时,set 方法将被调用,该方法可以自定义设置值的具体方式,set 默认值为 undefined
INFO

需要注意的是,不能同时设置value,writeableget set

我们可以通过Object.defineProperty(操作单个)与Object.defineProperties(操作多个)来修改这些特性值。

code.ts
1
// 三个参数分别为 target, key, descriptor(特性值的描述对象)
2
Object.defineProperty(person, 'name', {
3
value: "TOM"
4
})
5
6
// 新增
7
Object.defineProperty(person, 'age', {
8
value: 20
9
})

装饰器语法与此类似,当我们想要自定义一个装饰器时,可以这样写:

code.ts
1
function nameDecorator(target, key, descriptor) {
2
descriptor.value = () => {
3
return 'jake';
4
}
5
return descriptor;
6
}
7

函数 nameDecorator 的定义会重写被他装饰的属性(getName)。方法的三个参数与 Object.defineProperty 一一对应,分别指当前的对象 Person,被作用的属性getName,以及属性特性值的描述对象 descriptor 。函数最后必须返回descriptor

使用时也很简单,如下:

code.ts
1
class Person {
2
constructor() {
3
this.name = 'jake'
4
}
5
@nameDecorator
6
getName() {
7
return this.name;
8
}
9
}
10
11
let p1 = new Person();
12
console.log(p1.getName())
13

getName 方法前面加上 @nameDecorator,就是装饰器语法。

自定义函数 nameDecorator 的参数中,target,就是装饰的对象Person,key 就是被装饰的具体方法getName

不能使用装饰器对构造函数进行更改,如果要修改构造函数,则可以通过如下的方式来完成

code.ts
1
function initDecorator(target, key, descriptor) {
2
const fn = descriptor.value;
3
// 改变传入的参数值
4
descriptor.value = (...args) => {
5
args[0] = 'TOM';
6
return fn.apply(target, args);
7
}
8
return descriptor;
9
}
10
11
class Person {
12
constructor(name, age) {
13
this.init(name, age)
14
}
15
@initDecorator
16
init(name, age) {
17
this.name = name;
18
this.age = age;
19
}
20
getName() {
21
return this.name;
22
}
23
getAge() {
24
return this.age;
25
}
26
}
27
28
console.log(new Person('alex', 20).getName()); // TOM

如果希望装饰器传入一个指定的参数,可以如下做。

code.ts
1
// 注意这里的差别
2
function initDecorator(name) {
3
return function (target, key, descriptor) {
4
const fn = descriptor.value;
5
descriptor.value = (...args) => {
6
args[0] = name;
7
return fn.apply(target, args);
8
}
9
return descriptor;
10
}
11
}
12
13
class Person {
14
constructor(name, age) {
15
this.init(name, age)
16
}
17
@initDecorator('xiaoming')
18
init(name, age) {
19
this.name = name;
20
this.age = age;
21
}
22
getName() {
23
return this.name;
24
}
25
getAge() {
26
return this.age;
27
}
28
}
29
30
console.log(new Person('alex', 20).getName()); // xiaoming

这里利用了闭包的原理,将装饰器函数外包裹一层函数,以闭包的形式缓存了传入的参数。

我们也可以对整个class添加装饰器

code.ts
1
function personDecorator(target) {
2
// 修改方法
3
target.prototype.getName = () => {
4
return 'hahahahaha'
5
}
6
// 新增方法,因为内部使用了this,因此一定不能使用箭头函数
7
target.prototype.getAge = function () {
8
return this.age
9
}
10
return target;
11
}
12
13
@personDecorator
14
class Person {
15
constructor(name, age) {
16
this.init(name, age)
17
}
18
init(name, age) {
19
this.name = name;
20
this.age = age;
21
}
22
getName() {
23
return this.name;
24
}
25
}
26
27
var p = new Person('alex', 30);
28
console.log(p.getName(), p.getAge()); // hahahahaha 30

也可以传参数

code.ts
1
function initDecorator(person) {
2
return function (target, key, descriptor) {
3
var method = descriptor.value;
4
descriptor.value = () => {
5
var ret = method.call(target, person.name);
6
return ret;
7
}
8
}
9
}
10
11
@stuDecorator(xiaom)
12
class Student {
13
constructor(name, age) {
14
this.init(name, age);
15
}
16
@initDecorator(xiaom)
17
init(name, age) {
18
this.name = name;
19
this.age = age;
20
}
21
getAge() {
22
return this.age;
23
}
24
getName() {
25
return this.name;
26
}
27
}
28
29
var p = new Student('hu', 18);
30
console.log(p.getAge(), p.getName(), p.getOther()); // 22 "xiaom" "other info."

那么用ES7 的decorator来实现最开始的需求,则可以这样做

code.ts
1
import { cloth, weapon, shoes, defaultRole } from './config';
2
3
// 基础角色
4
class Role {
5
constructor(role) {
6
this.hp = role.hp;
7
this.atk = role.atk;
8
this.speed = role.speed;
9
this.cloth = role.cloth;
10
this.weapon = role.weapon;
11
this.shoes = role.shoes;
12
this.nickname = role.nickname
13
this.gender = role.gender
14
}
15
run() { }
16
attack() { }
17
}
18
19
20
function ClothDecorator(target) {
21
target.prototype.getCloth = function (cloth) {
22
this.hp += cloth.hp;
23
this.cloth = cloth.name;
24
}
25
}
26
27
function WeaponDecorator(target) {
28
target.prototype.getWeapon = function (weapon) {
29
this.atk += weapon.attack;
30
this.weapon = weapon.name;
31
}
32
target.prototype.attack = function () {
33
if (this.weapon) {
34
console.log(`装备了${this.weapon},攻击更强了`);
35
} else {
36
console.log('战士的基础攻击');
37
}
38
}
39
}
40
41
function ShoesDecorator(target) {
42
target.prototype.getShoes = function (shoes) {
43
this.speed += shoes.speed;
44
this.shoes = shoes.name;
45
}
46
target.prototype.run = function () {
47
if (this.shoes) {
48
console.log(`穿上了${this.shoes},移动速度更快了`);
49
} else {
50
console.log('战士的奔跑动作');
51
}
52
}
53
}
54
55
56
@ClothDecorator
57
@WeaponDecorator
58
@ShoesDecorator
59
class Soldier extends Role {
60
constructor(role) {
61
const o = Object.assign({}, defaultRole, role);
62
super(o);
63
this.career = '战士';
64
if (role.hp == defaultRole.hp) {
65
this.hp = defaultRole.hp + 20;
66
}
67
if (role.speed == defaultRole.speed) {
68
this.speed = defaultRole.speed + 5;
69
}
70
}
71
run() {
72
console.log('战士的奔跑动作');
73
}
74
attack() {
75
console.log('战士的基础攻击');
76
}
77
}
78
79
const base = {
80
...defaultRole,
81
nickname: 'alex',
82
gender: 'man'
83
}
84
85
const s = new Soldier(base);
86
s.getCloth(cloth);
87
console.log(s);
88
89
s.getWeapon(weapon);
90
s.attack();
91
console.log(s);
92
93
s.getShoes(shoes);
94
s.run();
95
console.log(s);

这里需要注意的是,装饰者模式与直接使用浏览器支持的语法在实现上的一些区别。

ES7 Decorator重点在于对装饰器的封装,因此我们可以将上栗中的装饰器单独封装为一个模块。在细节上做了一些调整,让我们封装的装饰器模块不仅仅可以在创建战士对象的时候使用,在我们创建其他职业例如法师,射手的时候也能够正常使用。

code.ts
1
export function ClothDecorator(target) {
2
target.prototype.getCloth = function (cloth) {
3
this.hp += cloth.hp;
4
this.cloth = cloth.name;
5
}
6
}
7
8
export function WeaponDecorator(target) {
9
target.prototype.getWeapon = function (weapon) {
10
this.atk += weapon.attack;
11
this.weapon = weapon.name;
12
}
13
target.prototype.attack = function () {
14
if (this.weapon) {
15
console.log(`${this.nickname}装备了${this.weapon},攻击更强了。职业:${this.career}`);
16
} else {
17
console.log(`${this.career}的基本攻击`);
18
}
19
}
20
}
21
22
export function ShoesDecorator(target) {
23
target.prototype.getShoes = function (shoes) {
24
this.speed += shoes.speed;
25
this.shoes = shoes.name;
26
}
27
target.prototype.run = function () {
28
if (this.shoes) {
29
console.log(`${this.nickname}穿上了${this.shoes},移动速度更快了。职业:${this.career}`);
30
} else {
31
console.log(`${this.career}的奔跑动作`);
32
}
33
}
34
}

可以利用该例子,感受Decorator与继承的不同。

整理之后,Soldier的封装代码将会变得非常简单

code.ts
1
import { cloth, weapon, shoes, defaultRole } from './config';
2
import { ClothDecorator, WeaponDecorator, ShoesDecorator } from './equip';
3
import Role from './Role';
4
5
@ClothDecorator
6
@WeaponDecorator
7
@ShoesDecorator
8
class Soldier extends Role {
9
constructor(roleInfo) {
10
const o = Object.assign({}, defaultRoleInfo, roleInfo);
11
super(o);
12
this.career = '战士';
13
if (roleInfo.hp == defaultRoleInfo.hp) {
14
this.hp = defaultRoleInfo.hp + 20;
15
}
16
if (roleInfo.speed == defaultRoleInfo.speed) {
17
this.speed = defaultRoleInfo.speed + 5;
18
}
19
}
20
run() {
21
console.log('战士的奔跑动作');
22
}
23
attack() {
24
console.log('战士的基础攻击');
25
}
26
}
专栏首页
到顶
专栏目录