Jianglinyuan

2018-06-03 23:05

浅谈JS中的装饰器模式

本文作者:IMWeb Jianglinyuan 原文出处:IMWeb社区 未经同意,禁止转载

浅谈JS中的装饰器模式

什么是装饰器?

装饰器设计模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

我们通过下面的实例来演示装饰器模式的用法。其中,我们将把一个形状装饰上不同的颜色,同时又不改变形状类。

JS中的装饰器

装饰器(Decorator)是ES7中的一个新语法,使用可参考阮一峰的文章。正如其字面意思而言,它可以对类、方法、属性进行修饰,从而进行一些相关功能定制。它的写法与Java的注解(Annotation)非常相似,但是功能还是有很大区别。

JS中的Decorator在原理和功能上简单明了,简而言之就是对对象进行包装,返回一个新的对象描述(descriptor)。这个概念其实和React中的高阶组件也类似,大家可以用高阶组件的方式来理解它。

举个非常简单的例子:

假设我们现在要对一个函数log,打印出它的执行记录。

不使用Decorator:

const log = (srcFun) => {
  if(typeof(srcFun) !== 'function') {
    throw new Error(`the param must be a function`);
  }
  return (...arguments) => {
    console.info(`${srcFun.name} invoke with ${arguments.join(',')}`);
    srcFun(...arguments);
  }
}

const plus = (a, b) => a + b;

const logPlus = log(plus);

logPlus(1,2); // this will log : plus invoke with 1,2

使用Decorator

const log = (target, name, descriptor) => {
  var oldValue = descriptor.value;
  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };
  return descriptor;
}

class Math {
  @log  // Decorator
  plus(a, b) {
    return a + b;
  }
}
const math = new Math();

math.add(1, 2); // this will log: Calling plus with 1,2

从上面的代码可以看出,如果有的时候我们并不需要关心函数的内部实现,仅仅是想调用它的话,装饰器能够带来比较好的可读性,使用起来也是非常的方便。

JS中的原理

JS中的装饰器本质也是一个函数,利用的是JS中object的descriptor,这个函数会接收三个参数:

/**
 * 装饰器函数
 * @param {Object} target 被装饰器的类的原型
 * @param {string} name 被装饰的类、属性、方法的名字
 * @param {Object} descriptor 被装饰的类、属性、方法的descriptor
 */
function Decorator(target, name, descriptor) {
  // 以此可以获取实例化的时候此属性的默认值
  let v = descriptor.initializer && descriptor.initializer.call(this);

  // 返回一个新的描述对象作为被修饰对象的descriptor,或者直接修改 descriptor 也可以
  return {
    enumerable: true,
    configurable: true,
    get() {
      return v;
    },
    set(c) {
      v = c;
    },
  };
}

 // USE
 class Fudao{
    @Decorator
    title = "企鹅辅导“
 }

当然装饰器也可以接受参数:

// decorator 外部可以包装一个函数,函数可以带参数
function Decorator(type) {
  /**
* 装饰器函数
* @param {Object} target 被装饰器的类的原型
* @param {string} name 被装饰的类、属性、方法的名字
* @param {Object} descriptor 被装饰的类、属性、方法的descriptor
*/
  return (target, name, descriptor) => {
    // 以此可以获取实例化的时候此属性的默认值
    let v = descriptor.initializer && descriptor.initializer.call(this);

    // 返回一个新的描述对象作为被修饰对象的descriptor,或者直接修改 descriptor 也可以
    return {
      enumerable: true,
      configurable: true,
      get() {
        return v + type;
      },
      set(c) {
        v = c;
      },
    };
  }
}

// USE
 class Fudao{
    @Decorator('string') 
    title = "企鹅辅导“
 }

常见的装饰器

autobind

autobind修饰器使得方法中的this对象,绑定原始对象,使得this始终指向绑定的对象。

import { autobind } from 'core-decorators'; // a NPM lib

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let getPerson = person.getPerson;

getPerson() === person;
// true

readonly

readonly修饰器使得属性或方法不可写。

import { readonly } from 'core-decorators';

class Fudao {
  @readonly
  title = '企鹅辅导';
}

var fudao = new Fudao();
fudao.title = '腾讯课堂'; // This will log error & doesn't work

deprecate

deprecate可以用来装饰那些已经废弃的函数方法或者属性,这样用户在调用这个函数的时候就会收到相关的告警。

import { deprecate } from 'core-decorators';

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     See http://knowyourmeme.com/memes/facepalm for more details.
//

React中的装饰器

在React中我们可以使用装饰器来干我们想干的任何事情,这得益于React天生需要打包环境(虽然也可以不打包☺)。

transferProps

如果我们想把propTypes中没有声明的props提取出来,放在ohters这个key下面,实现类似下面的功能:

@transferProps
class Foo extends React.Component {

  static propTypes = {
    foo: PropTypes.string,
  }

  // 下面props中只会有foo和others两种
  render() {
    return (
      <div {...this.props.others}>
        {this.props.foo}
      </div>
    );
  }
}

怎么实现的呢,很简单:

import React from 'react';

export default function transferProps(Target) {
  return (props) => {
    const others = {};
    const newProps = Object.assign({}, props);
    Object.keys(newProps).forEach((key) => {
      if (!(Target.propTypes && Target.propTypes[key])) {
        others[key] = newProps[key];
        delete newProps[key];
      }
    });
    return <Target others={others} {...newProps} />;
  };
}

renameProps

改变props的name:

@renameProps({
  foo: 'bar',
})
class Foo extends React.Component {

  static propTypes = {
    bar: PropTypes.string,
  }

  render() {
    return (
      <div>
        {this.props.bar}
      </div>
    );
  }
}

// Rendering the following
// <Foo foo="example" />
//
// produces these props:
// props = {
//   bar: 'example',
// }

代码实现,也非常简单:

import React from 'react';

export default function renameProps(newNames) {
  return (Target) => {
    return (props) => {
      const newProps = Object.assign({}, props);
      const names = Object.keys(newNames);
      Object.keys(newProps).forEach((key) => {
        const nameIndex = names.indexOf(key);
        if (names && key && nameIndex !== -1) {
          newProps[newNames[names[nameIndex]]] = newProps[key];
          delete newProps[key];
        }
      });
      return <Target {...newProps} />;
    };
  };
}

@connect

在Redux中,通常我们需要一个reducer和一个action,然后使用connect来包裹你的Component。

import React from 'react'
import {render} from 'react-dom'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import action from 'action.js'

class App extends React.Component{
  render(){
    return <div>hello</div>
  }
}
function mapStateToProps(state){
  return state.main
}
function mapDispatchToProps(dispatch){
  return bindActionCreators(action,dispatch)
}
export default connect(mapStateToProps,mapDispatchToProps)(App)

使用connect装饰器,可以让代码变得非常明了:

import React from 'react'
import {render} from 'react-dom'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import action from 'action.js'

[@connect](/user/connect)(
 state=>state.main,
 dispatch=>bindActionCreators(action,dispatch)
)
class App extends React.Component{
  render(){
    return <div>hello</div>
  }
}

总结

Decorator 虽然原理非常简单,但是的确可以实现很多实用又方便的功能,像 mobx中@observable、Angular中的大量应用以及证明了其的高可用性。个人觉得在一些开发框架中尝试加入装饰器可以提供更简洁以及高效的代码质量,下篇我们将为你介绍装饰器的实际应用场景,带你体验装饰器的魅力。

博客文章地址

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。