1

从装饰模式到装饰器

装饰模式

装饰模式的作用是:在不修改原有的接口的情况下,让类表现的更好。
什么叫更好?

为什么需要装饰模式

自然是继承有一些问题
继承会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;
超类的内部细节对于子类是可见的,继承常常被认为破坏了封装性;

js中的装饰模式

js中的装饰模式实际上把一个对象A嵌入到另外一个对象B中,也就是B对象包装了A对象,形成了一个包装链。请求随着这一条包装链一次传递到了包装链上的所有对象,每一个对象都有机会处理这一个请求。

设想一下场景,打农药,我选了一个后羿,然后开始一级我学了第一个技能。


var HouYi = {
    skill : function() {
        console.log('增加了攻速')
    }
    HP :function(){
        return 1999
    }
}

HouYi.skill()

结果,自以为自己很6了的后羿去了下路,遇到了狄仁杰,二马一错蹬几个回合下来就后羿就凉了。后羿觉得不服,回家一趟,发奋努力,又学了第二个技能和第三个技能。

class Hero {
    buy(){
        console.log('有了一双鞋子')
        // console.log('有了一件复活甲')
        // console.log('有了一把饮血剑')
    }
}


var Houyi = new Hero()
Houyi.buy()

那么问题来了,我们看了一下,后羿还是回家买了新的武器装备,而不是寒酸的只有一双鞋。但是,我们看到,每次后羿买东西都要回家,也就是都要修改buy方法,那么怎么样在不回家,不修改buy的方法的基础上又把东西卖了呢,也就是,如何动态的买东西。

从英雄联盟过渡到王者荣耀。

也就是在代码运行期间,我们很难切入某个函数的执行环境。


class Hero {
    buyShoes(){
        console.log('有了一双鞋子')
    }
}

var Houyi = new Hero()

var buyAtk = function() {
    console.log('有了一把饮血剑')
}

var buyDef = function () {
    console.log('有了一件复活甲')
}

var buyShoes= Houyi.buy

Houyi.buybuybuy = function() {
    buyShoes()
    buyAtk()
    buyDef()
}

Houyi.buybuybuy()

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

装饰模式的缺点:缺点的话我们也能看到我们定义了很多很相似的细小对象到我们的命名空间中,这样使我们的架构变得十分的复杂,穷于管理。这就有可能导致,我们不是使用它而是被它使用。

它是一个语法糖

说完了装饰模式,我们再看一下在ES7中最新引入的装饰器(decorator)。这个概念其实是从python里引进的。

def my_decorator(fn):
  def inner(name):
    print 'Hello ' + fn(name)
  return inner

@my_decorator
def greet(name):
  return name

greet('Decorator!')
# Hello Decorator!

这种@decorator的写法其实就是一个语法糖。

语法糖意指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。不过其并没有给语言添加什么新东西。

所以,ES7中的 decorator 同样借鉴了这个语法糖,不过依赖于ES5的Object.defineProperty 方法 。

defineProperty 所做的事情就是,为一个对象增加新的属性,或者更改对象某个已存在的属性。调用方式是 Object.defineProperty(obj, prop, descriptor) ,这 3 个参数分别代表:

  1. obj: 目标对象
  2. prop: 属性名
  3. descriptor: 针对该属性的描述符

关于descriptor代表的意思是对象描述符,它本身一个对象,用于描述目标对象的一个属性的属性。

  • value:属性的值,默认为undefined
  • writable:能否修改属性的值,默认值为true
  • enumerable:能否通过for-in循环返回属性。默认为ture
  • configurable:能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性,默认为true.

关于最后一个configurable还要多说一句,当我们把对象的一个属性设置为false之后,我们就不能再二次修改了,也就是不能置成true,会报错。

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false,
  configurable:false
});

object1.property1 = 77;
// throws an error in strict mode

console.log(object1.property1);
// expected output: 42

装饰类

decorator大量用于装饰ES6中的类。具体的写法是:

@decoratorFun
class Base{}

function decoratorFun(target){
    target.bool = true
}

Base.bool  //   true

上面的代码,decorator就是一个装饰器,它装饰了Base这个类的行为(为其增加了一个静态属性)。而函数的target属性,就是被装饰的类本身。

所以,就是说,如果装饰器是一个对类做修改装饰的一般函数,那么他的第一个参数就是被装饰类的引用。但是,如果一个参数不够用,怎么办?

如果不够用,那么直接可以在外面再包一层。

function decorFun2(str){
    return function(target){
        target.name = str
    }
}

@decorFun2('zhangjingwei')
class Person {}
Person.name //   zhangjingwei

@decorFun2('susu')
class Person2 {}
Person2.name     // susu
class Foo1 {  
     classMethod() {  
        return 'hello';  
    }  
} 
class Foo2 {  
     static classMethod() {  
        return 'hello';  
    }  
} 

Foo1.classMethod()
var foo1 = new Foo1();  
foo1.classMethod() 

Foo2.classMethod()
var foo2 = new Foo2()
foo2.classMethod()
静态属性和实例属性。静态属性是类本身的属性,类生成的对象不能继承该属性,但是实例属性是类生成的对象可以继承的。
ES6的规范说,类里面没有静态属性,只有静态方法。

上面的都是给类添加静态属性,如果想要增加实例属性,那么可以操作类的原型。

function decorFun3(name){
    return function(target) {
        target.prototype.name = name
    }
}

@decorFun3('lubanqihao')
class Nongyao {}

let ny1 = new Nongyao()
ny1.name    // lubanqihao

装饰类的属性

装饰器不仅可以装饰类,还能装饰类的方法。

有的时候,我们想把类中的某个属性设置成只读不支持修改,可以来用装饰器来实现。

function readonly(target,name,descriptr){
    descriptor.writable = false
    return descriptor
}

class Cat{
    @readonly
    say(){
        console.log('miaomiao)
    }
}

let kitty = new Cat()
kitty.say = function(){
    console.log('wangwang')
}

kitty.say()      //miaomiao

我们看到通过装饰器给类中的say方法,设置成了只读。

参数有三个,target name descriptor。

第一个参数是类的原型对象,装饰器的本意是装饰类的实例,但是因为类的实例还没有生成,只能去修饰类的原型。第二个参数是要修饰的属性名。第三个参数是该属性的描述对象。

这个很眼熟是不是?

是不是有点类似于Object.defineProperty().其实装饰器对类的属性的作用,就是通过Object.defineProperty这个方法进行扩展和封装。

实际上装饰器的行为原理是这样的:

let descriptor = {
    value:function(){
        console.log('miaomiao)
    },
    enumerable:false,
    configable:true,
    writable:true
}

descriptor = readonly(Cat.protitype,'say',descriptor) || descriptor

Object.defineProperty(Cat.prototype, "say",descriptor);

所以,我们看到,当装饰器操作类本身的时候,操作的对象也是类本身,但装饰器操作的是类中的方法的时候,操作的对象是是类的描述符descriptor,因为类中的属性的全部信息都记录在这个描述符里面。

装饰器不能修饰方法,是因为存在变量提升,不会生效

core-decorators.js第三方模块

core-decorators.js是一个第三方模块,提供了一些常用的装饰器。

  • @autobind:这个装饰器的作用是自动绑定this对象。

    import {autobind} from 'core-decorators';
    
    class Person {
        @autobind
        getPerson() {
            return this
        }
    }
    
    let p1 = new Person()
    let getPerson = p1.getPerson()
    
    getPerson() === p1     // true
  • @readonly 修饰方法使方法变得不可写

    import {readonly} from 'core-decorators'
    
    class Dog {
        @readonly
        name = 'zhang'
    }
    
    let d1 = new Dog()
    d1.name = 'susu'    // 会报错,因为属性不可写
  • override 会检查子类的方法是否覆盖了父类的同名方法。如果不正确,会报错。
import {override} from 'core-decorators'

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  //   Did you mean "speak"?
}

Mixin混入

意思就是在一个对象中混入另外一个对象的方法,是对象继承的一种替代方式。

//一个混入的简单实现:

class Foo {
    foo() {
        console.log('hahaha')
    }
}

class Bar {}

Object.assign(Bar.prototype,Foo)
let bar1 = new Bar()

bar1.foo()

上边的例子是通过Object.assign方法进行的。Bar的实例都有foo方法。

所以呢,我们可以把混入的方法单独拿出来,结合装饰器使用。

// mixin.js

export function mixin(...list){
    return function (target) {
        Object.assign(target.prototype,...list)
    }
}
import {mixin} from './mixin.js

class Foo {
    foo() {
        console.log('lalala')
    }
}

@mixin(Foo)
class NewFoo {}

let nf = new NewFoo()
nf.foo()

这样一个不好的地方,就是他会改写新类的原型对象。

Babel转码

需要安装下边的东西:

npm install --save-dev babel-core babel-preset-stage-0

然后设置文件.babelrc

{
  "plugins": ["transform-decorators"]
}

这样,就能在代码里实现装饰器了。

参考资料https://github.com/zhiqiang21...
http://www.liuhaihua.cn/archi...


张小草1018
285 声望8 粉丝