ES7 decorator 从入门到放弃

  • Post author:
  • Post category:其他


0. 引言

平时我们用

decorator

来封装一些和原有类或者

react

组件(高阶组件)本身无关的功能。比如说埋点、路由、hack、复杂冗余的业务逻辑、以及扩展的功能等,非常好用。本文就怎么使用

decorator

,以及如何扩展及应用场景做下简单总结。

1. 准备工作

安装

babel

转码。

npm install --save-dev babel-cli babel-plugin-transform-decorators-legacy 

根目录配置.babelrc

{
  "presets": ["env"],
  "plugins": ["transform-decorators-legacy"]
}

package.json:

// npm init 后的package.json
{
    "name": "my-project",
    "version": "1.0.0",
    "scripts": {
     // 写入
     "build": "./node_modules/.bin/babel 你的原es6.js -o 转成的es5.js"
     // 目录写法
     // "build": "./node_modules/.bin/babel src -d lib"
    },
    "devDependencies": {
      "babel-cli": "^6.0.0"
    }
  }

babel-cli安装在全局的话,就不需要按照上面在以上script中添加build了。

运行:

npm run build // 全局babel-cli 使用 babel 你的原es6.js > 转成的es5.js

即可生成可运行的es5文件,然后使用

node file.js

在node里运行,或者引入html中即可。

当然也可以直接在

babel网站

中编辑转码。


decorator

可以装饰

对象



属性

,下面就分开介绍。

2. decorator 装饰属性

装饰属性,会在属性注册到类prototype之前先执行装饰器,先看源码怎么写。

// 定义一个装饰器函数,里面的target这些参数和Object.defineProperty是对应的
function ready(target, name, descripter) {
  descripter.writable = false;  // 改写writable属性
  return descripter;            // 注意,返回属性描述对象descriptor
}
class A() {
  @ready
  b() {
    console.log('f');
  }
}

再看babel转义成es5的核心代码:

function applyDecorator(target, property, decorators, descriptor, context) {
  // 定义一个新的描述对象
  var desc = {};
  // 将target.property 的descriptor挂载在desc上
  // 从后往前依次覆盖前面的desc上的属性
  desc = decorators.slice().reverse().reduce(function(desc, decorator) {
    return decorator(target, propery, desc) || desc;
  });

  return desc;
}
applyDecorator(A.prototype, 'b', [ready], Object.getOwnPropertyDescriptor(A.prototype, 'b'), A.prototype);

经过ready处理后的descriptor 返回到类

A(Class)

的原型上的属性

b

上,之后对

b

方法的重写将会被禁止,这样我们可以针对

b

方法进行一些限制、拦截和改造性质的操作,包括

get



set

方法。

另外,这里的

reduce

用的非常好。将含有处理函数的数组倒置,,然后使用

reduce

,每次处理函数返回值作为下一次的第一个属性再次传入,下一个处理函数继续处理返回值,

redux

库插件处理state就是这种思路。

3. decorator 装饰对象

装饰对象是大家使用decorator最广泛的场景。

function filter(flag) {
  return function(target) {
    // handler flag 
    // 也可以写一些自定义的方法挂载在target上
    target.getName = function() {
      return 'wf'
    };
  }
}

@filter(true)
class A {
  constructor() {
    this.name = "my"
  }
  getName() {
    return this.name;
  }
}

const a = new A();
console.log(a.getName()); // wf

最终转化成es5,直接传入 A,内部改写A的方法。

A = filter(true)(A);

也就是说,装饰器通过传入对象或者对象及方法名,通过改写他的行为。

改写可以通过

Object.defineProperty

,也可以通过

target.otherName

的方式,定义和赋值的区别,

defineProperty

这种能设置属性的特性,限制扩展等。而直接赋值会受到访问器属性的

get``set

的影响,也会影响访问器属性。

这样我们就能在上面能访问内部的属性,同时也可以添加许多扩展性的功能,这里不作扩展说明,后续设计模式类的文章会介绍。

这里·额外·说下babel转码后的代码里有 这种括号

const a = (m, n, v)

运算符。

var a = function(v) {
  return function(v){
    return v + 'v';
  }
}

var m;
var b = (m = a('v'), m('wf'), ''); // ''

以上代码最终返回空, ()里会依次从前到后计算,最后返回最后一个值!!! 是不是有点像reduce,不过人家无法传递参数,只能指定固定的表达式。

4. 实践中引入decorator

下面我们开始使用

decorator

去实践一些东西。比如以形容我们(程序员)为例, 哈哈哈。

我们先建立一个

基类

,从一个错误的示例开始(熟悉原型链的可以直接略过),了解下日常写

decorator

的坑:

class Programmer {
  constructor(hasGirlF) {
    this.hasGirlF = !!hasGirlF; // 女朋友
  }
}

下面来实例化并为他添加特殊属性:

function fallInLove(flag = false) {
  return function(target) {
    target.hasGirlF = flag; 
  }
}

@fallInLove(true)   // 嘿嘿
class Programmer {
  constructor(hasGirlF) {
    this.hasGirlF = !!hasGirlF;
  }
}
const wf = new Programmer();
wf.hasGirlF;   // false ???
Programmer.hasGirlF; // true

为什么改为true了,结果还是没有女朋友!!!

因为上面也有提到,

decorator

传入的

target

其实就是

Programmer

这个类,

target.hasGirlF

其实修改的是这个类的静态属性,而不是实例化后的

wf

因此,这里需要改进,

class

中定义的方法其实就是该类的原型方法,我们可以尝试为原型添加属性:

function fallInLove(flag = false) {
  return function(target) {
    target.prototype.hasGirlF = flag;  // 注意prototype
  }
}
@fallInLove(true)   // 嘿嘿
class Programmer {
  constructor(hasGirlF) {
    this.hasGirlF = !!hasGirlF; // 未定义hasGirlF为false
  }
}
const wf = new Programmer();
wf.hasGirlF;   // false  ???
Programmer.hasGirlF; // undefined
Programmer.prototype.hasGirlF; // true 

还是不正常。其实是因为根据原型链继承的思想,先查找实例中的

hasGirlF

,再沿着原型链往上找。实例中有

hasGirlF

了,就不会往原型链上找了。


对此总结下上面的错误


  • es6

    类的

    decorator

    中传入的

    target

    是类本身,而不是它的原型,所以直接在上面添加方法是无法被实例引用到的
  • 不要在

    decorator

    中添加和个体(实例)相关的属性,因为修改原型会影响到每个实例,并且很有可能被构造函数覆盖

开始正确的示例,先给大家添加点共性的属性:

function addProps(...props) {
  return function(target) {
    (target.prototype.props = []).push(...props);
  }
}

@addProps('聪明')
class Programmer {
  constructor(hasGirlF) {
    this.hasGirlF = !!hasGirlF;
  }
  fallInLove(flag = false) {
    this.hasGirlF = flag;
  }
}
const wf = new Programmer();
wf.props; // 聪明
const my = new Programmer();
my.props; // 聪明 bingo!

以上能用

addProps

给所有实例添加共有的属性

聪明

,同时通过实例方法

fallInlove

也能设置实例自身

hasGillF

,保证了可复用和扩展。

5. 使用mixin扩展decorator

以上,一个新增的属性通过装饰器

addProps

添加到

Programmer

类上去了。

但是,如果想添加、覆盖一堆新方法,或者想复制另一类的方法,那这一个个地添加岂不是很麻烦。

这个时候, 我们需要用到

mixin



extends

。下面我们先通过

mixin

来复制其他对象的行为。

const mixin = (behaviour) =>
  target => {
    Object.assign(target.prototype, behaviour);
    return target;  // 一定要return target 不能返回prototype,其为对象
  }

@mixin({
  props: [],
  addProps: function(...props) {
    this.props.push(...props);
  }
})
class Programmer {
  constructor(hasGirlF) {
    this.hasGirlF = !!hasGirlF;
  }
  fallInLove(flag = false) {
    this.hasGirlF = flag;
  }
}

const wf = new Programmer();
wf.props; // []
wf.addProps('乐观');
wf.props; // ['乐观']

这样,

Programmer

类就能使用

mixin

中调用的类和方法了。

但是这里mixin中引入对象的行为都是

可枚举的

,为了让

mixin

功能更贴近

class

,这里存在两个小问题:

  • 真正的es6的

    class

    中定义的行为是不可枚举的

  • Object.assign

    只会复制可枚举的属性和方法

在此我们转换下,使其和

class

一致:

const mixin = (behaviour) => 
  target => {
    for (let property of Reflect.ownKeys(behaviour)) {  // ownKeys相比Object.keys能遍历出class中方法(不可枚举属性)
      Object.defineProperty(target.prototype, property, { value: behaviour[property] })
    }
    return target;
  }

6. 使用extends代替decorator

其实上面能做到的,

extends

都能做到,而且更加透明易懂。

class Behavior extends Programmer {
  addProps: function(...props) {
    this.props.push(...props);
  }
}

搞定!!!


extends

能轻松地搞定这些继承问题,是不是感觉上面

mixin

结合

decorator

的写法很鸡肋?


但是

,在某些情况下,这里使用

mixin



extends

都有不足,那就是无论是哪种方法,因为合成(mixin)和继承(extend),都会有一个类被影响,丧失了原有的纯粹性:


  • mixin

    覆盖和新增了

    Programmer

    中原型的方法

  • extends

    使

    Behaviour

    需要带入

    Programmer

    中的方法,使其无法被其他类型的类复用

这里我们用一种新的方式取代:

const mixin = (target, Behaviour) => {
  const newTarget = class extends target {}
  for (let property of Reflect.ownKeys(Behaviour)) {
    Object.defineProperty(newTarget.prototype, property, { value: Behaviour[property] })
  }
  return newTarget;
}
const newBehavior = mixin(Programmer, Behaviour), 

以上,通过在

mixin

内部定义一个类继承

Programmer

类,再把

Behaviour

的方法复制到它上面,

Behaviour

这个类没有混入

Programmer

中的方法,

Programmer

也没有被

Behaviour

的方法覆盖,这样

Programmer



Behaviour

本身都不会被影响,同时又合成了一个共有属性的新类,基于此,

decorator

能实现的以上都可以实现,利用

decorator

可以这样写。

const compose = (Behaviour) => (Programmer) => mixin(Programmer, Behaviour);
@compose(behaviour)
class OtherClass {}

个人认为,以上方法比较适用于已有代码扩展,在两个类都保持独立的情况下添加额外的方法来集成,不同的应用场景下选择是各不相同的。很多

decorator

直接通过

extends

写更方便,不需要外面再套一层

decorator

,再在

decorator



extends

,但是

decorator

存在的优势就是

mixin

作为函数的存在,其中传入对象参数的时候更灵活,可以实现


多重继承





自定义


。另外,像

依赖链



不可见



复杂性

这种不足,其实

extends



mixin

是差不多的。

7. decorator引入React


react



decorator

其实是利用高阶组件(HOC)来完成的,这里使用PP(Props Proxy)作为例子。(关于HOC的文章很多,可以看看

这篇

const Log = (WrappedCompoent) => class extends React.Component {
  // 扩展的业务逻辑
  // 可访问App的this
  render() {
    return (<WrappedComponent 
     otherProps={this.otherProps}
     {...this.props} />);
  }
}

@Log
class App extends React.Component {
// 业务逻辑
  constructor(props) {
    super(props);
    // 可以取到this.props.otherProps
  }
}

这里Log传入的就是App类,在不影响App组件的正常情况下,App中还可以获取高阶组件中定义的方法和属性,同时高阶组件也能做额外的一些事情,特别方便。

参考文献


  1. Functional Mixins in ECMAScript 2015.raganwald

    作者的文章写的非常好!



版权声明:本文为xhydwufu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。