当我们想要扩展一个对象的能力时,可以采用的方案有
前面三种方法,都不可避免的会修改原有对象的代码。
而如果不修改原有对象代码的情况下,装饰者模式是很好的一种解决方案。
首先,我们有设计了几件装备,他们的信息保存在 config.js
中
10// config.js20export const cloth = {30name: '七彩炫光衣',40hp: 100050}60export const weapon = {70name: '青龙偃月刀',80attack: 200090}10export const shoes = {11name: '神行疾步靴',12speed: 30013}14export const defaultRole = {15hp: 100,16atk: 50,17speed: 125,18cloth: null,19weapon: null,20shoes: null,21career: null,22gender: null23}
然后创建一个基础的角色对象,添加基础的属性与方法
10// 基础角色类20// 有血条,攻击力,速度三个基础属性30// 以及衣服,武器,鞋子三个装备插槽40class Role {50constructor(role) {60this.hp = role.hp;70this.atk = role.atk;80this.speed = role.speed;90this.cloth = role.cloth;10this.weapon = role.weapon;11this.shoes = role.shoes;12}13run() {}14attack() {}15}
然后基于基础角色类创建职业为战士的角色类
10// 战士20class Soldier extends Role {30constructor(role) {40const r = Object.assign({}, defaultRole, role)50super(r)60this.nickname = r.nickname70this.gender = r.gender80this.career = '战士'90// 战士的基础血条 +2010if (role.hp == defaultRole.hp) {11this.hp = defaultRole.hp + 2012}13// 战士的基础移动速度 +514if (role.speed == defaultRole.speed) {15this.speed = defaultRole.speed + 516}17}18run() {19console.log('战士的奔跑动作')20}21attack() {22console.log('战士的攻击动作')23}24}
接下来,我们要创建装饰类。
装饰类不必知道被装饰类的存在。他是相对独立的。我们可以称之为装饰者。
装饰类的关键,是以被装饰者的实例,作为初始化参数。
装饰类可能会有许多,衣服武器鞋子等都可以各设计一个装饰类分别负责不同的行为与变化。因此我们需要设计一个基础装饰类作为各装饰类的父类,用于减少代码量。
装饰类与被装饰类的属性与方法基本保持一致,只是实现上略有差异。
10// 基础装饰类20class Decorator {30constructor(role) {40this.role = role;50this.hp = role.hp;60this.atk = role.atk;70this.speed = role.speed;80this.cloth = role.cloth;90this.weapon = role.weapon;10this.shoes = role.shoes;11this.career = role.career;12this.gender = role.gender;13this.nickname = role.nickname;14}15run() {16this.role.run()17}18attack() {19this.role.attack()20}21}
接下来创建衣服装饰类 ClothDecorator
衣服装饰类继承基础装饰类
衣服只会修改角色的属性,并不会修改角色的行为
1class ClothDecorator extends Decorator {2constructor(role, cloth) {3super(role)4this.cloth = cloth.name5this.hp += cloth.hp6}7}
类封装好了之后,我们使用一下,感受一下变化
10const baseInfo = {...defaultRole, nickname: 'alex', gender: 'man'}20// 创建一个战士角色30const alex = new Soldier(baseInfo)40alex.run()50alex.attack()60console.log(alex)7080alex = new ClothDecorator(alex, cloth)90// 查看变化10console.log(alex)
我们看属性 cloth 与 hp 已经发生了变化。
除此之外,我们还需要创建武器装饰类,鞋子装饰类。
武器与鞋子的穿戴会改变角色的攻击动作与奔跑动作,因此需要做更多的修改
10class WeaponDecorator extends Decorator {20constructor(role, weapon) {30super(role)40this.weapon = weapon.name50this.atk += weapon.attack60}70attack() {80console.log('装备了武器,攻击速度变得更快了')90}10}
1console.log('--------装备武器-----------')2alex = new WeaponDecorator(alex, weapon)3alex.attack()4console.log(alex)
鞋子装饰类
10class ShoesDecorator extends Decorator {20constructor(role, shoes) {30super(role)40this.shoes = shoes.name50this.speed += shoes.speed60}70run() {80console.log('穿上了鞋子,奔跑速度变得更快了')90}10}
1console.log('--------装备鞋子-----------')2alex = new ShoesDecorator(alex, shoes)3alex.run()4console.log(alex)
除此之外,我们玩游戏时,还知道每个角色都会在某些情况下获得不同的 buff,例如大龙 buff,小龙 buff,红蓝 buff 等,这些buff 有的会更改角色属性,例如 cd 更短,攻击更高,有的会更改攻击特性,例如红 buff 会持续掉血,减速等。
我们可以思考一下如何实现这些功能。
默认情况下并不支持装饰器语法,因此,在学习该语法之前,你需要找到一个支持该语法的开发环境 如何在构建环境中支持 decorator
ES7 中提供了一个快捷的语法用来解决与装饰者模式一样的问题。这就是装饰器语法 decorator。
在学习装饰器语法之前,需要先温习一下 ES5 的一些基础知识。
假设有对象如下:(便于理解)
1var person = {2name: 'TOM'3}
对象中的每个属性都有一个特性值来描述这个属性的特点,他们分别是:
configurable
: 属性是否能被 delete 删除,当值为false时,其他特性值也不能被改变,默认值为trueenumerable
: 属性是否能被枚举,也就是是否能被 for in 循环遍历。默认为 truewritable
: 是否能修改属性值。默认为 truevalue
:具体的属性值是多少,默认为 undefinedget
:当我们通过person.name
访问 name 的属性值时,get 将被调用。该方法可以自定义返回的具体值是多少。get 默认值为 undefinedset
:当我们通过person.name = 'Jake'
设置 name 属性值时,set 方法将被调用,该方法可以自定义设置值的具体方式,set 默认值为 undefined需要注意的是,不能同时设置
value,writeable
与get set
。
我们可以通过Object.defineProperty
(操作单个)与Object.defineProperties
(操作多个)来修改这些特性值。
1// 三个参数分别为 target, key, descriptor(特性值的描述对象)2Object.defineProperty(person, 'name', {3value: "TOM"4})56// 新增7Object.defineProperty(person, 'age', {8value: 209})
装饰器语法与此类似,当我们想要自定义一个装饰器时,可以这样写:
1function nameDecorator(target, key, descriptor) {2descriptor.value = () => {3return 'jake';4}5return descriptor;6}7
函数 nameDecorator
的定义会重写被他装饰的属性(getName)。方法的三个参数与 Object.defineProperty
一一对应,分别指当前的对象 Person
,被作用的属性getName
,以及属性特性值的描述对象 descriptor
。函数最后必须返回descriptor
。
使用时也很简单,如下:
10class Person {20constructor() {30this.name = 'jake'40}50@nameDecorator60getName() {70return this.name;80}90}1011let p1 = new Person();12console.log(p1.getName())13
在 getName
方法前面加上 @nameDecorator
,就是装饰器语法。
自定义函数 nameDecorator
的参数中,target,就是装饰的对象Person,key 就是被装饰的具体方法getName
。
不能使用装饰器对构造函数进行更改,如果要修改构造函数,则可以通过如下的方式来完成
10function initDecorator(target, key, descriptor) {20const fn = descriptor.value;30// 改变传入的参数值40descriptor.value = (...args) => {50args[0] = 'TOM';60return fn.apply(target, args);70}80return descriptor;90}1011class Person {12constructor(name, age) {13this.init(name, age)14}15@initDecorator16init(name, age) {17this.name = name;18this.age = age;19}20getName() {21return this.name;22}23getAge() {24return this.age;25}26}2728console.log(new Person('alex', 20).getName()); // TOM
如果希望装饰器传入一个指定的参数,可以如下做。
10// 注意这里的差别20function initDecorator(name) {30return function (target, key, descriptor) {40const fn = descriptor.value;50descriptor.value = (...args) => {60args[0] = name;70return fn.apply(target, args);80}90return descriptor;10}11}1213class Person {14constructor(name, age) {15this.init(name, age)16}17@initDecorator('xiaoming')18init(name, age) {19this.name = name;20this.age = age;21}22getName() {23return this.name;24}25getAge() {26return this.age;27}28}2930console.log(new Person('alex', 20).getName()); // xiaoming
这里利用了闭包的原理,将装饰器函数外包裹一层函数,以闭包的形式缓存了传入的参数。
我们也可以对整个class添加装饰器
10function personDecorator(target) {20// 修改方法30target.prototype.getName = () => {40return 'hahahahaha'50}60// 新增方法,因为内部使用了this,因此一定不能使用箭头函数70target.prototype.getAge = function () {80return this.age90}10return target;11}1213@personDecorator14class Person {15constructor(name, age) {16this.init(name, age)17}18init(name, age) {19this.name = name;20this.age = age;21}22getName() {23return this.name;24}25}2627var p = new Person('alex', 30);28console.log(p.getName(), p.getAge()); // hahahahaha 30
也可以传参数
10function initDecorator(person) {20return function (target, key, descriptor) {30var method = descriptor.value;40descriptor.value = () => {50var ret = method.call(target, person.name);60return ret;70}80}90}1011@stuDecorator(xiaom)12class Student {13constructor(name, age) {14this.init(name, age);15}16@initDecorator(xiaom)17init(name, age) {18this.name = name;19this.age = age;20}21getAge() {22return this.age;23}24getName() {25return this.name;26}27}2829var p = new Student('hu', 18);30console.log(p.getAge(), p.getName(), p.getOther()); // 22 "xiaom" "other info."
那么用ES7 的decorator来实现最开始的需求,则可以这样做
10import { cloth, weapon, shoes, defaultRole } from './config';2030// 基础角色40class Role {50constructor(role) {60this.hp = role.hp;70this.atk = role.atk;80this.speed = role.speed;90this.cloth = role.cloth;10this.weapon = role.weapon;11this.shoes = role.shoes;12this.nickname = role.nickname13this.gender = role.gender14}15run() { }16attack() { }17}181920function ClothDecorator(target) {21target.prototype.getCloth = function (cloth) {22this.hp += cloth.hp;23this.cloth = cloth.name;24}25}2627function WeaponDecorator(target) {28target.prototype.getWeapon = function (weapon) {29this.atk += weapon.attack;30this.weapon = weapon.name;31}32target.prototype.attack = function () {33if (this.weapon) {34console.log(`装备了${this.weapon},攻击更强了`);35} else {36console.log('战士的基础攻击');37}38}39}4041function ShoesDecorator(target) {42target.prototype.getShoes = function (shoes) {43this.speed += shoes.speed;44this.shoes = shoes.name;45}46target.prototype.run = function () {47if (this.shoes) {48console.log(`穿上了${this.shoes},移动速度更快了`);49} else {50console.log('战士的奔跑动作');51}52}53}545556@ClothDecorator57@WeaponDecorator58@ShoesDecorator59class Soldier extends Role {60constructor(role) {61const o = Object.assign({}, defaultRole, role);62super(o);63this.career = '战士';64if (role.hp == defaultRole.hp) {65this.hp = defaultRole.hp + 20;66}67if (role.speed == defaultRole.speed) {68this.speed = defaultRole.speed + 5;69}70}71run() {72console.log('战士的奔跑动作');73}74attack() {75console.log('战士的基础攻击');76}77}7879const base = {80...defaultRole,81nickname: 'alex',82gender: 'man'83}8485const s = new Soldier(base);86s.getCloth(cloth);87console.log(s);8889s.getWeapon(weapon);90s.attack();91console.log(s);9293s.getShoes(shoes);94s.run();95console.log(s);
这里需要注意的是,装饰者模式与直接使用浏览器支持的语法在实现上的一些区别。
ES7 Decorator重点在于对装饰器的封装,因此我们可以将上栗中的装饰器单独封装为一个模块。在细节上做了一些调整,让我们封装的装饰器模块不仅仅可以在创建战士对象的时候使用,在我们创建其他职业例如法师,射手的时候也能够正常使用。
10export function ClothDecorator(target) {20target.prototype.getCloth = function (cloth) {30this.hp += cloth.hp;40this.cloth = cloth.name;50}60}7080export function WeaponDecorator(target) {90target.prototype.getWeapon = function (weapon) {10this.atk += weapon.attack;11this.weapon = weapon.name;12}13target.prototype.attack = function () {14if (this.weapon) {15console.log(`${this.nickname}装备了${this.weapon},攻击更强了。职业:${this.career}`);16} else {17console.log(`${this.career}的基本攻击`);18}19}20}2122export function ShoesDecorator(target) {23target.prototype.getShoes = function (shoes) {24this.speed += shoes.speed;25this.shoes = shoes.name;26}27target.prototype.run = function () {28if (this.shoes) {29console.log(`${this.nickname}穿上了${this.shoes},移动速度更快了。职业:${this.career}`);30} else {31console.log(`${this.career}的奔跑动作`);32}33}34}
可以利用该例子,感受Decorator与继承的不同。
整理之后,Soldier的封装代码将会变得非常简单
10import { cloth, weapon, shoes, defaultRole } from './config';20import { ClothDecorator, WeaponDecorator, ShoesDecorator } from './equip';30import Role from './Role';4050@ClothDecorator60@WeaponDecorator70@ShoesDecorator80class Soldier extends Role {90constructor(roleInfo) {10const o = Object.assign({}, defaultRoleInfo, roleInfo);11super(o);12this.career = '战士';13if (roleInfo.hp == defaultRoleInfo.hp) {14this.hp = defaultRoleInfo.hp + 20;15}16if (roleInfo.speed == defaultRoleInfo.speed) {17this.speed = defaultRoleInfo.speed + 5;18}19}20run() {21console.log('战士的奔跑动作');22}23attack() {24console.log('战士的基础攻击');25}26}