ES7 decorator 从入门到放弃

来源:互联网 发布:房卡 娄底放炮罚 源码 编辑:程序博客网 时间:2024/06/05 13:50

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; // undefinedProgrammer.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} />);  }}@Logclass App extends React.Component {// 业务逻辑  constructor(props) {    super(props);    // 可以取到this.props.otherProps  }}

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

参考文献

  1. Functional Mixins in ECMAScript 2015.raganwald 作者的文章写的非常好!
原创粉丝点击