ES7 decorator 从入门到放弃

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方法进行一些限制、拦截和改造性质的操作,包括getset方法。

另外,这里的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类上去了。
但是,如果想添加、覆盖一堆新方法,或者想复制另一类的方法,那这一个个地添加岂不是很麻烦。
这个时候, 我们需要用到mixinextends。下面我们先通过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的写法很鸡肋?

但是,在某些情况下,这里使用mixinextends都有不足,那就是无论是哪种方法,因为合成(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的方法覆盖,这样ProgrammerBehaviour本身都不会被影响,同时又合成了一个共有属性的新类,基于此,decorator能实现的以上都可以实现,利用decorator可以这样写。

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

个人认为,以上方法比较适用于已有代码扩展,在两个类都保持独立的情况下添加额外的方法来集成,不同的应用场景下选择是各不相同的。很多decorator直接通过extends写更方便,不需要外面再套一层decorator,再在decoratorextends,但是decorator存在的优势就是mixin作为函数的存在,其中传入对象参数的时候更灵活,可以实现多重继承自定义。另外,像依赖链不可见复杂性这种不足,其实extendsmixin是差不多的。

7. decorator引入React

reactdecorator其实是利用高阶组件(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 作者的文章写的非常好!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值