js 装饰器_一文说透JS装饰器

f8a5427901c5b2a92bed4b27dc33074f.png

装饰器是什么?

装饰器(Decorator)是ES7中的一个提案,可能在将来会成为规范。

许多面向对象的编程语言中都具有该项功能,如Java、Python等。

装饰器本身是一种与类相关的语法,主要用来注释或修改类和类方法。许多面向对象的语言都支持这项功能。


装饰器的作用?

Decorator 如其名“装饰器”,可以对一些 对象 进行装饰然后返回一个被包装过的对象,可以被装饰的对象包括:类、类方法和访问器。

通过一系列的例子来感受下装饰器的作用。

装饰类

@speakName 
class Hero {}  

function speakName(target, name, descriptor){  
  target.prototype.name = 'hero'; 
}  

const hero =new Hero(); 
console.log(hero.name); // hero

speak 作为装饰器修饰了 Hero 类,在类的原型上添加了 name 属性。当实例化Hero 类时,实例上拥有了name 属性。

如果需要将speak 装饰器做得更加灵活,可以给speak 增加参数的方式来实现。

@speakName('Julius') 
class Hero {}  

function speakName(name) {  
  return function(target, name, descriptor) {  
    target.prototype.name = name;   
  }; 
}  

const hero = new Hero(); 
console.log(hero.name); // Julius

可以看到speak 装饰器接受参数,并将参数作为添加name 属性的值。

由此,可以知晓装饰器是支持传参与不传参的

装饰类属性(类方法)

class Hero {  name = "Julius";  

  @readOnly  
  speak() {  
    console.log(`My name is ${this.name}`);   
  } 
}  

function readOnly(target, name, descriptor) {  
  descriptor.writable = false; 
}  

const hero = new Hero(); 
hero.speak();   
// My name is Julius hero.speak = ()=>{};    
// Error: Cannot assign to read only property 'speak' of object

这里给 Hero 类的 speak 类方法添加了 readOnly 装饰器,如果想修改实例化后的 speak 类方法就会报错。

需要注意的是,装饰器目前仅支持装饰类属性中的类方法,无法装饰类中的属性变量,如上例中的 name 属性。

参数详解

可以发现,上述例子中的每个装饰器都具备三个参数,即 target , name , descriptor

首先,依据上述两个例子,分别看下target , name , descriptor 的内容。

@speakName("Julius") 
class Hero {}  

function speakName(kingname) {  
  return function(target, name, descriptor) {  
    console.log(target);    // ƒ Hero()  
    console.log(name);      // undefined  
    console.log(descriptor);    // undefined  
    target.prototype.name = kingname;   
  }; 
}

装饰器在装饰类时,target 目标是类本身,也就是例子中的 Hero 类,而此时namedescriptor 均为 undefined

class Hero {  
  name = "Julius";  
  @readonly  
  speak() {  
    console.log(`My name is ${this.name}`);   
  } 
}  

function readonly(target, name, descriptor) {  
  console.log(target);  // {} __proto__: Object: Object  
  console.log(name);    // speak  
  console.log(descriptor);    
  /*    
   * configurable: true      
   * enumerable: false      
   * value: ƒ speak()      
   * writable: true      
   */  
  descriptor.writable = false; 
}  

const hero = new Hero();

装饰器在装饰类方法时,target 目标是类实例,name 是类方法的名称,descriptor 则是类方法的描述对象,可看到 value 是类方法本身,其他3个值和 Object.defineProperty的属性一样,负责控制值的行为,该例中的 writable 就是其中之一。

装饰器的实现原理

我们看一下装饰类属性的例子通过Babel编译为ES5后的源码。

// 装饰器方法 
function _decorate(decorators, factory, superClass, mixins) {  
  // ... 省略具体实现 
}  

var Hero = _decorate(null, function (_initialize) {  
  var Hero = function Hero() {  
    _classCallCheck(this, Hero);   
    _initialize(this);   
  };   
  return {  
    F: Hero,  
    d: [           
        {  kind: "field",  
           key: "name",  
           value: function value() {return "Julius";}           
        }, 
        {  kind: "method",  
           decorators: [readOnly],  
           key: "speak",  
           value: function speak() {console.log("My name is ".concat(this.name));}           
        }             
       ]     
    }; 
});  

function readOnly(target, name, descriptor) {  
  descriptor.writable = false; 
}  

var hero = new Hero(); 
hero.speak();

尝试使用脑图来解读下这段源码:

2cfa9592bc3d879f9449a72eb0249e9d.png

就上图,着重解释一下factory参数内容:

  • _decorate 装饰器方法被执行,factory参数传入了一个匿名函数。
  • 匿名函数体中,Hero 类被编译成了 function,并返回对象 {F:类方法,d:[{类原有属性}]}
  • 类原有属性中 decorators 字段为装饰方法( readOnly 函数)的数组。

decorators 数组中的函数最终将映射到Object.defineProperty操作对象的属性。此处readOnly(target, name, descriptor){} 修改的是属性的可写入性。

同时由于decorators 是数组,即可支持多个装饰方法作用于同一个对象属性,下面举例来看下。

class Hero {
  name = "Julius";

  @enumerable(false)
  @readonly  
  speak() {
    console.log(`My name is ${this.name}`);
  }
}

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

function enumerable(isEnumerable) {
  return function(target, key, descriptor) {
    descriptor.enumerable = isEnumerable;
  };
}

const hero = new Hero();
hero.speak();


for (var o in hero) {
  console.log(o);
}

//  My name is Julius
//  name
//  由于descriptor.enumerable=false,故speak属性无法被遍历

装饰器的使用场景

通过装饰器的使用,可以让日常代码变得更加优雅与抽象。下面以React使用场景为例,做下简单介绍。

下面的代码以“方法调用时需打印日志”的场景为例,采用装饰器的方式完成该功能。

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  constructor(...props) {
    super(...props);
    this.state = {
      count: 1
    };
  }

  @log()
  onClick(count) {
    this.setState({
      count: Number(count) + 1
    });
  }
  render() {
    return (
      <div>
        <button onClick={() => this.onClick(this.state.count)}>
          乘以{this.state.count}
        </button>
        <ul>
          <li>{1 * this.state.count}</li>
          <li>{2 * this.state.count}</li>
          <li>{3 * this.state.count}</li>
        </ul>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

function log() {
  return function decorator(target, name, descriptor) {
    if (typeof descriptor === "undefined") {
      return;
    }

    const value = descriptor.value; // onClick方法函数
    function overWriteValue(...args) {
      console.log(`log:执行方法${name};参数为${args}`);
      return value.bind(this)(args);
    }

    return {
      ...descriptor,
      value: overWriteValue // 拦截并重写onClick方法
    };
  };
}

// 执行后,可以在日志中看到下面的内容
// log:执行方法onClick;参数为1
// log:执行方法onClick;参数为2
// log:执行方法onClick;参数为3
// log:执行方法onClick;参数为4
// log:执行方法onClick;参数为5
react-descriptor - StackBlitz​stackblitz.com

小结

本文介绍了装饰器的原理、作用及使用场景,通过它能帮助我们在编码过程中实现更多实用且方便的功能。

希望在装饰器提案定案后,能有更好的使用体验,也能有更多的抽象实践。


如果觉得文章能帮助你理解一些前端知识点,请关注微信公众号。

fb65ab6f12a317c92501a0c9fd8e6e8f.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值