参考

提示

  • TypeScript 已经完整的实现了装饰器,js装饰器则是一个尚在提案中的语法,如果使用js而非ts,则需要配置Babel才能使用
  • 阅读此文前需要了解的前置知识:js类,Object.defineProperty,js原型,闭包
  • 文章介绍的装饰器是基于ts实现的装饰器

常用的装饰器举例

我们抛开装饰器是怎么实现的,先来看两个装饰器的例子,以便对装饰器有初步的了解

示例一:readonly

class Person {
  constructor(name) {
    this.name = name
  }
  getName() { return this.name }
}

上面的代码定义了一个类Person,它有一个原型方法getName,显然getName是可以被修改的

Person.prototype.getName = fuction() { return `哈哈哈` }
console.log(new Person('test').getName()); // 哈哈哈

假如我现在要求getName不能被修改,使用装饰器可以达到效果

// 这是readonly的具体实现,可以先忽略,后面会详细介绍
function readonly(target, name, descriptor) {
  descriptor.writable = false;
}

class Person {
  constructor(name) {
    this.name = name;
  }
  @readonly
  getName() {
    return this.name;
  }
}
// 下面语句会报错,提示getName readonly
Person.prototype.getName = function () {
  return `哈哈哈`;
};
console.log(new Person('test').getName());

上面的代码,我们给getName方法的定义上面加了一行@readonly,这就是装饰器的写法,非常直观,就像注释一样,一眼就看出getName这个方法是只读的,不能修改

示例二:deprecate

当我们调用第三方库的方法的时候,常常会在控制台看到一些警告提示这个方法即将被移除,使用装饰器可以达到这个效果

import { deprecate } from 'core-decorators'; // 是一个第三方模块,提供了几个常见的装饰器

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

const person = new Person();
person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.
person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming
person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     See http://knowyourmeme.com/memes/facepalm for more details.
//

当我们调用被deprecate装饰器装饰的方法facepalm时,控制台显示一条警告,表示该方法将废除,可以给deprecate传入不同的参数控制显示内容

通过上面的两个例子,我们可以看出,装饰器优雅的改变了类原来的行为,它是可复用的而且表达直观

装饰器是什么

装饰器本质是一种函数,写成@ + 函数名,它可以增加或修改类的功能

装饰器的一个使用场景是将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。比如上例中的readonly和deprecate,他们的功能很多类都可能用到,但是又与类本身无关,就很适合用装饰器来实现

装饰器的用法

首先明确装饰器并不能在代码的任意位置使用,它只能用于类,可以放在类定义、类属性、类方法、访问器、参数(包含类构造函数和方法)的前面

类装饰器(放在类定义的前面)

基本上,装饰器的行为就是下面这样

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。

示例三:使用装饰器给类添加静态属性

// 定义一个装饰器函数
const addPropertyA = (target) => {
  // 此处的target为类本身(即ClassA)
  target.a = 'a'; // 给类(ClassA)添加一个静态属性
};

@addPropertyA
class ClassA {
  constructor() {
    this.a = 1;
  }
}
console.info('ClassA.a: ', ClassA.a); // a
console.info('a: ', new ClassA().a); // 1

tips:可以使用在线编译ts将上面的ts代码编译成的js(es2017)帮助理解

示例四:使用装饰器给类添加实例属性

本例给类ClassA添加了原型方法

const addMethodTest = (target) => {
  target.prototype.test = () => {
    console.log('test');
  };
};

@addMethodTest
class ClassA {}

new ClassA().test(); // test

示例五:传参的类装饰器

示例三中,我们给ClassA这个类添加了一个静态属性a, 它的值是字符串'a'(一个写死的字面量),如果属性a的值需要由参数传入呢?此时需要在装饰器外再封装一层函数

function addPropertyA(value) { // 这是一个装饰器工厂
  return function (target) { // 这是装饰器
    target.a = value;
  };
}
// 箭头函数写法
// const addPropertyA = (value) => (target) => {
//   target.a = value;
// };

@addPropertyA('test')
class ClassA {}
console.info('ClassA.a: ', ClassA.a); // test

上面的写法等同于

function getAddPropertyA(value) {
  return function (target) {
    target.a = value;
  };
}
const addPropertyA = getAddPropertyA('test');

@addPropertyA
class ClassA {}
console.info('ClassA.a: ', ClassA.a); // test

方法装饰器(放在类方法定义的前面)

示例六:readonly

再来看文章开头的示例一,我们有一个Person类

class Person {
  constructor(name) {
    this.name = name
  }
  getName() { return this.name }
}

假如我现在要求getName不能被修改,可以改用Object.defineProperty来定义getName

class Person {
  constructor(name: string) {
    this.name = name;
  }
}
Object.defineProperty(Person.prototype, 'getName', {
  writable: false,
  value() {
    return this.name;
  },
});
// 下面语句会报错,提示getName readonly
Person.prototype.getName = function () {
  return `哈哈哈`;
};
console.log(new Person('test').getName());
function readonly(target, name, descriptor) {
  descriptor.writable = false;
}

class Person {
  constructor(name) {
    this.name = name;
  }
  @readonly
  getName() {
    return this.name;
  }
}
// 下面语句会报错,提示getName readonly
Person.prototype.getName = function () {
  return `哈哈哈`;
};
console.log(new Person('test').getName());

以上两种写法达到了同样的效果,但是第二种明显更优雅

readonly是装饰器函数,第一个参数是类的原型对象,上例是Person.prototype(注意不同于作用于类的装饰器),第二个参数是所要装饰的属性名(上例是getName),第三个参数是该属性的描述对象(同Object.defineProperty中传入的描述对象)。

上面代码说明,装饰器(readonly)可以修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性

示例七:属性装饰器

装饰器不仅可以作用于类方法,也可以作用于普通的类属性

import { deprecate } from 'core-decorators'; // 是一个第三方模块,提供了几个常见的装饰器

class Person {
  @readonly name = 'Jack';
}

const person = new Person();
person.name = 'Rose'; // TypeError: Cannot assign to read only property 'name' of object '#<Person>'

其他

参数装饰器本文未做介绍


suri
39 声望1 粉丝