优先使用对象组合而不是类继承。 --《设计模式》
1.什么是装饰者模式javascript
定义:动态的给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。html
2.装饰者模式参与者java
Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
ConcreteComponent:定义具体对象,即被装饰者
Decorator:抽象装饰者,继承自Component,从外类来扩展ConcreteComponent。对于ConcreteComponent来讲,不须要知道Decorator的存在,Decorator是一个接口或抽象类
ConcreteDecorator:具体装饰者,用于扩展ConcreteComponent
注:装饰者和被装饰者对象有相同的超类型,由于装饰者和被装饰者必须是同样的类型,这里利用继承是为了达到类型匹配,而不是利用继承得到行为。react
利用继承设计子类,只能在编译时静态决定,而且全部子类都会继承相同的行为;利用组合的作法扩展对象,就能够在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:类应该对扩展开放,对修改关闭。利用装饰者,咱们能够实现新的装饰者增长新的行为而不用修改现有代码,而若是单纯依赖继承,每当须要新行为时,还得修改现有的代码。git
3.javascript 如何使用装饰者模式
javascript 动态语言的特性使得使用装饰器模式十分的简单,文章主要内容会介绍两种使用装饰者模式的实际例子。es6
咱们都知道高阶函数是什么, 高阶组件实际上是差很少的用法,只不过传入的参数变成了react组件,并返回一个新的组件.github
A higher-order component is a function that takes a component and returns a new component.
形如:编程
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高阶组件是react应用中很重要的一部分,最大的特色就是重用组件逻辑。它并非由React API定义出来的功能,而是由React的组合特性衍生出来的一种设计模式。
若是你用过redux,那你就必定接触太高阶组件,由于react-redux中的connect就是一个高阶组件。redux
先来一个最简单的高阶组件设计模式
import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; class Usual extends Component { render() { console.log(this.props, 'props'); return ( <div> Usual </div> ) } } export default simpleHoc(Usual); import React, { Component } from 'react'; const simpleHoc = WrappedComponent => { console.log('simpleHoc'); return class extends Component { render() { return <WrappedComponent {...this.props}/> } } } export default simpleHoc;
组件Usual经过simpleHoc的包装,打了一个log... 那么形如simpleHoc就是一个高阶组件了,经过接收一个组件class Usual,并返回一个组件class。 其实咱们能够看到,在这个函数里,咱们能够作不少操做。 并且return的组件一样有本身的生命周期,function,另外,咱们看到也能够把props传给WrappedComponent(被包装的组件)。
实现高阶组件的方法有两种
属性代理(props proxy)。高阶组件经过被包裹的 React 组件来操做 props。
反向继承(inheritance inversion)。高阶组件继承于被包裹的 React 组件。
属性代理
引入里咱们写的最简单的形式,就是属性代理(Props Proxy)的形式。经过hoc包装wrappedComponent,也就是例子中的Usual,原本传给Usual的props,都在hoc中接受到了,也就是props proxy。 由此咱们能够作一些操做
1.操做props
最直观的就是接受到props,咱们能够作任何读取,编辑,删除的不少自定义操做。包括hoc中定义的自定义事件,均可以经过props再传下去。
import React, { Component } from 'react'; const propsProxyHoc = WrappedComponent => class extends Component { handleClick() { console.log('click'); } render() { return (<WrappedComponent {...this.props} handleClick={this.handleClick} />); } }; export default propsProxyHoc;
而后咱们的Usual组件render的时候, console.log(this.props) 会获得handleClick.
2.refs获取组件实例
当咱们包装Usual的时候,想获取到它的实例怎么办,能够经过引用(ref),在Usual组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。
import React, { Component } from 'react'; const refHoc = WrappedComponent => class extends Component { componentDidMount() { console.log(this.instanceComponent, 'instanceComponent'); } render() { return (<WrappedComponent {...this.props} ref={instanceComponent => this.instanceComponent = instanceComponent} />); } }; export default refHoc;
3.抽离state
这里不是经过ref获取state, 而是经过 { props, 回调函数 } 传递给wrappedComponent组件,经过回调函数获取state。这里用的比较多的就是react处理表单的时候。一般react在处理表单的时候,通常使用的是受控组件(文档),即把input都作成受控的,改变value的时候,用onChange事件同步到state中。固然这种操做经过Container组件也能够作到,具体的区别放到后面去比较。看一下代码就知道怎么回事了:
import React, { Component } from 'React'; const MyContainer = (WrappedComponent) => class extends Component { constructor(props) { super(props); this.state = { name: '', 4 }; this.onNameChange = this.onNameChange.bind(this); } onNameChange(event) { this.setState({ name: event.target.value, }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange, }, } return <WrappedComponent {...this.props} {...newProps} />; } }
在这个例子中,咱们把 input 组件中对 name prop 的 onChange 方法提取到高阶组件中,这样就有效地抽象了一样的 state 操做。
反向继承
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { return super.render(); } }
正如所见,高阶组件返回的组件继承于 WrappedComponent。由于被动地继承了 WrappedCom- ponent,全部的调用都会反向,这也是这种方法的由来。
这种方法与属性代理不太同样。它经过继承 WrappedComponent 来实现,方法能够经过 super 来顺序调用。由于依赖于继承的机制,HOC 的调用顺序和队列是同样的:
didmount→HOC didmount→(HOCs didmount)→will unmount→HOC will unmount→(HOCs will unmount)
在反向继承方法中,高阶组件可使用 WrappedComponent 引用,这意味着它可使用WrappedComponent 的 state、props 、生命周期和 render 方法。但它不能保证完整的子组件树被解析。
1.渲染劫持
渲染劫持指的就是高阶组件能够控制 WrappedComponent 的渲染过程,并渲染各类各样的结 果。咱们能够在这个过程当中在任何 React 元素输出的结果中读取、增长、修改、删除 props,或 读取或修改 React 元素树,或条件显示元素树,又或是用样式控制包裹元素树。
正如以前说到的,反向继承不能保证完整的子组件树被解析,这意味着将限制渲染劫持功能。 渲染劫持的经验法则是咱们能够操控 WrappedComponent 的元素树,并输出正确的结果。但若是 元素树中包括了函数类型的 React 组件,就不能操做组件的子组件。
咱们先来看条件渲染的示例:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { if (this.props.loggedIn) { return super.render(); } else { return null; } } }
第二个示例是咱们能够对 render 的输出结果进行修改:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { const elementsTree = super.render(); let newProps = {}; if (elementsTree && elementsTree.type === 'input') { newProps = {value: 'may the force be with you'}; } const props = Object.assign({}, elementsTree.props, newProps); const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children); return newElementsTree; } }
在这个例子中,WrappedComponent 的渲染结果中,顶层的 input 组件的 value 被改写为 may the force be with you。所以,咱们能够作各类各样的事,甚至能够反转元素树,或是改变元素 树中的 props。这也是 Radium 库构造的方法。
2.控制state
高阶组件能够读取、修改或删除 WrappedComponent 实例中的 state,若是须要的话,也能够 增长 state。但这样作,可能会让 WrappedComponent 组件内部状态变得一团糟。大部分的高阶组 件都应该限制读取或增长 state,尤为是后者,能够经过从新命名 state,以防止混淆。
咱们来看一个例子:
const MyContainer = (WrappedComponent) => class extends WrappedComponent { render() { return ( <div> <h2>HOC Debugger Component</h2> <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre> <p>State</p> <pre>{JSON.stringify(this.state, null, 2)}</pre> {super.render()} </div> ); } }
在这个例子中,显示了 WrappedComponent 的 props 和 state,以方便咱们在程序中去调试它们。
高阶组件能够看作是装饰器模式(Decorator Pattern)在React的实现。即容许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种
ES7中添加了一个decorator的属性,使用@符表示,能够更精简的书写。那上面的例子就能够改为:
import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; @simpleHoc export default class Usual extends Component { render() { return ( <div> Usual </div> ) } } //simple-hoc const simpleHoc = WrappedComponent => { console.log('simpleHoc'); return class extends Component { render() { return <WrappedComponent {...this.props}/> } } }
和高阶组件是一样的效果。
类的装饰
@testable class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true
上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类自己。
若是以为一个参数不够用,能够在装饰器外面再封装一层函数。
function testable(isTestable) { return function(target) { target.isTestable = isTestable; } } @testable(true) class MyTestableClass {} MyTestableClass.isTestable // true @testable(false) class MyClass {} MyClass.isTestable // false
上面代码中,装饰器 testable 能够接受参数,这就等于能够修改装饰器的行为。
方法的装饰
装饰器不只能够装饰类,还能够装饰类的属性。
class Person { @readonly name() { return `${this.first} ${this.last}` } }
上面代码中,装饰器 readonly 用来装饰“类”的name方法。
装饰器函数 readonly 一共能够接受三个参数。
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);
装饰器第一个参数是 类的原型对象,上例是 Person.prototype,装饰器的本意是要“装饰”类的实例,可是这个时候实例还没生成,因此只能去装饰原型(这不一样于类的装饰,那种状况时target参数指的是类自己);
第二个参数是 所要装饰的属性名
第三个参数是 该属性的描述对象
另外,上面代码说明,装饰器(readonly)会修改属性的 描述对象(descriptor),而后被修改的描述对象再用来定义属性。
ES5 中,mixin 为 object 提供功能“混合”能力,因为 JavaScript 的原型继承机制,经过 mixin 一个或多个对象到构造器的 prototype上,可以间接提供为“类”的实例混合功能的能力。
下面是例子:
function mixin(...objs){ return objs.reduce((dest, src) => { for (var key in src) { dest[key] = src[key] } return dest; }); } function createWithPrototype(Cls){ var P = function(){}; P.prototype = Cls.prototype; return new P(); } function Person(name, age, gender){ this.name = name; this.age = age; this.gender = gender; } function Employee(name, age, gender, level, salary){ Person.call(this, name, age, gender); this.level = level; this.salary = salary; } Employee.prototype = createWithPrototype(Person); mixin(Employee.prototype, { getSalary: function(){ return this.salary; } }); function Serializable(Cls, serializer){ mixin(Cls, serializer); this.toString = function(){ return Cls.stringify(this); } } mixin(Employee.prototype, new Serializable(Employee, { parse: function(str){ var data = JSON.parse(str); return new Employee( data.name, data.age, data.gender, data.level, data.salary ); }, stringify: function(employee){ return JSON.stringify({ name: employee.name, age: employee.age, gender: employee.gender, level: employee.level, salary: employee.salary }); } }) );
从必定程度上,mixin 弥补了 JavaScript 单一原型链的缺陷,能够实现相似于多重继承的效果。在上面的例子里,咱们让 Employee “继承” Person,同时也“继承” Serializable。有趣的是咱们经过 mixin Serializable 让 Employee 拥有了 stringify 和 parse 两个方法,同时咱们改写了 Employee 实例的 toString 方法。
咱们能够以下使用上面定义的类:
var employee = new Employee("jane",25,"f",1,1000); var employee2 = Employee.parse(employee+""); //经过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false
ES6 中的 mixin 式继承
在 ES6 中,咱们能够采用全新的基于类继承的 “mixin” 模式设计更优雅的“语义化”接口,这是由于 ES6 中的 extends 能够继承动态构造的类,这一点和其余的静态声明类的编程语言不一样,在说明它的好处以前,咱们先看一下 ES6 中如何更好地实现上面 ES5 代码里的 Serializable:
用继承实现 Serializable
class Serializable{ constructor(){ if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class Person extends Serializable{ constructor(name, age, gender){ super(); Object.assign(this, {name, age, gender}); } } class Employee extends Person{ constructor(name, age, gender, level, salary){ super(name, age, gender); this.level = level; this.salary = salary; } static stringify(employee){ let {name, age, gender, level, salary} = employee; return JSON.stringify({name, age, gender, level, salary}); } static parse(str){ let {name, age, gender, level, salary} = JSON.parse(str); return new Employee(name, age, gender, level, salary); } } let employee = new Employee("jane",25,"f",1,1000); let employee2 = Employee.parse(employee+""); //经过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false 上面的代码,咱们用 ES6 的类继承实现了 Serializable,与 ES5 的实现相比,它很是简单,首先咱们设计了一个 Serializable 类: class Serializable{ constructor(){ if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } }
它检查当前实例的类上是否有定义 stringify 和 parse 静态方法,若是有,使用静态方法重写 toString 方法,若是没有,则在实例化对象的时候抛出一个异常。
这么设计挺好的,但它也有不足之处,首先注意到咱们将 stringify 和 parse 定义到 Employee 上,这没有什么问题,可是若是咱们实例化 Person,它将报错:
let person = new Person("john", 22, "m"); //Uncaught ReferenceError: Please define stringify method to the Class!
这是由于咱们没有在 Person 上定义 parse 和 stringify 方法。由于 Serializable 是一个基类,在只支持单继承的 ES6 中,若是咱们不须要 Person 可序列化,只须要 Person 的子类 Employee 可序列化,靠这种继承链是作不到的。
另外,如何用 Serializable 让 JS 原生类的子类(好比 Set、Map)可序列化?
因此,咱们须要考虑改变一下咱们的设计模式:
用 mixin 实现 Serilizable
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class Person { constructor(name, age, gender){ Object.assign(this, {name, age, gender}); } } class Employee extends Serializable(Person){ constructor(name, age, gender, level, salary){ super(name, age, gender); this.level = level; this.salary = salary; } static stringify(employee){ let {name, age, gender, level, salary} = employee; return JSON.stringify({name, age, gender, level, salary}); } static parse(str){ let {name, age, gender, level, salary} = JSON.parse(str); return new Employee(name, age, gender, level, salary); } } let employee = new Employee("jane",25,"f",1,1000); let employee2 = Employee.parse(employee+""); //经过序列化反序列化复制对象 console.log(employee2, employee2 instanceof Employee, //true employee2 instanceof Person, //true employee == employee2); //false
在上面的代码里,咱们改变了 Serializable,让它成为一个动态返回类型的函数,而后咱们经过 class Employ extends Serializable(Person) 来实现可序列化,在这里咱们没有可序列化 Person 自己,而将 Serializable 在语义上变成一种修饰,即 Employee 是一种可序列化的 Person。因而,咱们要 new Person 就不会报错了:
let person = new Person("john", 22, "m"); //Person {name: "john", age: 22, gender: "m"}
这么作了以后,咱们还能够实现对原生类的继承,例如:
继承原生的 Set 类
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } class MySet extends Serializable(Set){ static stringify(s){ return JSON.stringify([...s]); } static parse(data){ return new MySet(JSON.parse(data)); } } let s1 = new MySet([1,2,3,4]); let s2 = MySet.parse(s1 + ""); console.log(s2, //Set{1,2,3,4} s1 == s2); //false
经过 MySet 继承 Serializable(Set),咱们获得了一个可序列化的 Set 类!一样咱们还能够实现可序列化的 Map:
class MyMap extends Serializable(Map){ ... static stringify(map){ ... } static parse(str){ ... } }
若是不用 mixin 模式而使用继承,咱们就得分别定义不一样的类来对应 Set 和 Map 的继承,而用了 mixin 模式,咱们构造出了通用的 Serializable,它能够用来“修饰”任何对象。
咱们还能够定义其余的“修饰符”,而后将它们组合使用,好比:
const Serializable = Sup => class extends Sup { constructor(...args){ super(...args); if(typeof this.constructor.stringify !== "function"){ throw new ReferenceError("Please define stringify method to the Class!"); } if(typeof this.constructor.parse !== "function"){ throw new ReferenceError("Please define parse method to the Class!"); } } toString(){ return this.constructor.stringify(this); } } const Immutable = Sup => class extends Sup { constructor(...args){ super(...args); Object.freeze(this); } } class MyArray extends Immutable(Serializable(Array)){ static stringify(arr){ return JSON.stringify({Immutable:arr}); } static parse(data){ return new MyArray(...JSON.parse(data).Immutable); } } let arr1 = new MyArray(1,2,3,4); let arr2 = MyArray.parse(arr1 + ""); console.log(arr1, arr2, arr1+"", //{"Immutable":[1,2,3,4]} arr1 == arr2); arr1.push(5); //throw Error!
上面的例子里,咱们经过 Immutable 修饰符定义了一个不可变数组,同时经过 Serializable 修饰符修改了它的序列化存储方式,而这一切,经过定义 class MyArray extends Immutable(Serializable(Array)) 来实现。