修饰器详解

达芬奇密码2018-07-18 12:56

本文基本内容:

  • 什么是修饰器?
  • 修饰器和Mixins
  • 在create-react-app构建的项目使用修饰器

什么是修饰器?

Decorators make it possible to annotate and modify classes and properties at design time.

While ES5 object literals support arbitrary expressions in the value position, ES6 classes only support literal functions as values. Decorators restore the ability to run code at design time, while maintaining a declarative syntax.

A decorator is:

  • an expression
  • that evaluates to a function
  • that takes the target, name, and decorator descriptor as arguments
  • and optionally returns a decorator descriptor to install on the target object

装饰器允许你在类和方法定义的时候去注释或者修改它。装饰器是一个作用于函数的表达式,它接收三个参数 targetnamedescriptor , 然后可选性的返回被装饰之后的 descriptor 对象。当一个函数需要扩展时,或者多个函数需要共享一个功能的时候,最有效的办法或许只能修改原来的函数。so,修饰器就派上用场了。

修饰器的主要两个用法: 1.修饰属性 先看一个基础类:

class Person {
    sayHello(){
        return `${this.name} says Hello!` ;
    }
}

通过编译之后,其实就是为Person.prototype添加一个sayHello的属性描述。

Object.defineProperty( Person.prototype , 'sayHello' , {
    value: ... , // sayHello方法
    enumerable: false ,
    configurable: true ,
    writable: true
} )

假如我们需要把一个属性变成只读,可以定义这样一个@readonly修饰器,位于定义的属性的语法之前。

function readonly( target , key , descriptor ){
    descriptor.writable = false ;
}

定义之后如下使用:

class Person {
    @readonly
    sayHello(){
        return `${this.name} says Hello!` ;
    }
}

在注册属性描述符之前,Javascript先调用:

let descriptor = {
    value: ... ,  // sayHello方法
    enumerable: false ,
    configurable: true ,
    writable: true ,
}
// 修饰器的参数和Object.defineProperty一致;在调用相应的defineProperty之前,有机会修改descriptor
descriptor = readonly( Person.prototype , 'sayHello' , descriptor ) || descriptor ;

当你尝试修改修改sayHello这个方法:

var xiaoming = new Person() ;
xiaoming.sayHello = function(){
    console.log( "I don't want to say Hello!" ) ;
}
// Uncaught TypeError: Cannot assign to read only property 'sayHello' of object '#<Person>'

修饰器的几个参数介绍:

2.修饰类 根据规范,修饰器接收构造函数(target)做为参数。使用方式如下:

function superPerson( target ){
    target.prototype.sayHello = ()=>console.log( `Hi , I'm superPerson` ) ;
}

@superPerson
class Person {
    sayHello(){
        return `${this.name} says Hello!` ;
    }
}

const xiaoming = new Person() ;

xiaoming.sayHello()    // Hi , I'm superPerson

这个可扩展性很强,使我们能够用修饰器功来实现类工厂:

function superPerson( config ){
    return target=>{
        target.prototype.sayHello = ()=>console.log( `Hi , I'm superPerson , I can ${config.feature}` ) ;
    }
}

@superPerson( { feature: 'fly'} )
class Person {
    sayHello(){
        return `${this.name} says Hello!` ;
    }
}

let xiaoming = new Person() ;

xiaoming.sayHello()             // Hi , I'm superPerson , I can fly

@superPerson( { feature: 'swimming'} )
class Person2 {
    sayHello(){
        return `${this.name} says Hello!` ;
    }
}

xiaoming = new Person2() ;

xiaoming.sayHello()             // Hi , I'm superPerson , I can swimming

基本

修饰器和Mixins

当已经写好的类在项目中已经频繁使用之后,修改类的行为,需要小心修改。有没有一种方法,可以完成一个小的功能,但是不需要修改原有的类呢?或者说怎么把很小的功能点从类中拆分出去,看如下代码:

function mixin(behaviour, sharedBehaviour = {}) {
    const instanceKeys = Reflect.ownKeys(behaviour);
    const sharedKeys = Reflect.ownKeys(sharedBehaviour);
    const typeTag = Symbol('isa') ;
    function _mixin(clazz) {
        for (let property of instanceKeys){
            Object.defineProperty( clazz.prototype , property , {
                value: behaviour[ property ] ,
                writable: true ,
            } ) ;
        }
        Object.defineProperty( clazz.prototype, typeTag , {
            value: true
        } ) ;
        return clazz;
    }
    for (let property of sharedKeys){
        Object.defineProperty(_mixin, property, {
            value: sharedBehaviour[property] ,
            enumerable: sharedBehaviour.propertyIsEnumerable(property)
        } ) ;
    }
    Object.defineProperty( _mixin , Symbol.hasInstance , {
        value: (i) =>{ return !!i[typeTag] ; }
    } ) ;
    return _mixin ;
}

这个mixin帮助函数,可以混合行为到类当中,而且非常方便的提供了“静态”方法,函数最后的Symbol.hasInstance.hasInstance是为了instance instanceof constructor工作。

const Hobbies = mixin( {
  addHobby ( hobby ) {
    this.hobbies().push( hobby );
    return this ;
  },
  hobbies () {
    return this.hobbiesArr || (this.hobbiesArr = []);
  }
} , { sayHi: ()=>{ console.log( 'hi!' ) } } ) ;

@Hobbies
class Person {
  constructor (first, last) {
    this.rename(first, last);
  }
  fullName () {
    return this.firstName + " " + this.lastName;
  }
  rename (first, last) {
    this.firstName = first;
    this.lastName = last;
    return this;
  }
};

const xiaoMing = new Person( 'xiao' , 'ming' ) ;


xiaoMing
  .addHobby( "football" )
  .addHobby( "basketball" ) ;

console.log( xiaoMing.hobbies() )   // ["football", "basketball"]

Hobbies.sayHi()      // "hi!"

获取兴趣列表和添加兴趣的两种行为,通过minxin帮助函数和修饰器,添加到了Person类的行为中。极大的方便了代码的复用和灵活性

在create-react-app构建的项目使用修饰器

安装插件:yarn add babel-plugin-transform-decorators-legacy 把react的配置暴露出来,执行:yarn eject 修改/config/webpack.config.dev.js文件:

{
    test: /\.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    options: {
      "plugins": ["transform-decorators-legacy"] ,  // 增加这一行配置
    },
},

然后就可以愉快的使用了:

import React, { Component } from 'react';

window.__locals = { userName: 'xxx' , userMail: 'xxx@corp.netease.com' } ;

function decorator( Component ){
    let __locals = Object.assign( {} , window.__locals ) ;
    return props=>{
        return <Component { ...props } __locals={ __locals } /> ;
    } ;
}

@decorator
class App extends Component {
  render() {
    console.log( this.props.__locals )
    return <div>hello world!</div>
    }
}

export default App;

简单的演示了一下react中修饰器的用法,项目中常用到的个人用户信息,特别是SPA项目,经常会由后台将登陆的用户信息写入到window对象中,然后取值的话,可有通过修饰器封装一层,然后传入需要用到的类中。避免每次都window获取,防止被篡改。

这里遇到一个小坑,以为装了babel-plugin-transform-decorators就可以使用了,发现会报错

Syntax error: Decorators are not officially supported yet in 6.x pending a proposal update.

However, if you need to use them you can install the legacy decorators transform with:

报错信息给出了只要装上面的一个就行了,这个插件可以删掉

参考资料

1.javascript-decorators

2.Using ES.later Decorators as Mixins

本文来自网易实践者社区,经作者毛庭峰授权发布。