es7Decorator装饰器的不完全指南

511 阅读6分钟

前言

本来是想分析mobx原理的,但是因为太菜了没办法一下子看懂mobx源码MobX 中是使用装饰器设计模式实现观察值的,要先掌握装饰器,才能进一步去理解观察值。因此今天就先缕一缕Decorator装饰器这个东东。

啥是装饰器呀

装饰器模式

说到装饰器,就不得不说说装饰器模式。它是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构,是作为对现有类的一个包装。

一般来说,在代码设计中,我们应当遵循「多用组合,少用继承」的原则。通过装饰器模式动态地给一个对象添加一些额外的职责。

使用了装饰器的类可以理解为穿上了机甲的tony stark,钢铁侠本质是一个人,机甲通过“装饰”来增强这个人的能力,不过再怎么装饰他还是一个人

装饰器Decorator

装饰器是es7 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。

下面是一个使用装饰器的简单例子,这个 @readonly 可以将 count 属性设置为只读。可以看出来,装饰器大大提高了代码的简洁性和可读性。

class Person {
    @readonly count = 0;
}

but,装饰器目前还是一个stage-2提案,没有被浏览器所支持,因此如果要使用的话需要在编译器中启用当前实验性的装饰器语法

  • 在 tsconfig.json 中启用编译器选项 "experimentalDecorators": true 。
  • npm i --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
  • 在.babelrc配置:
{ 
    "plugins": [
        ["@babel/plugin-proposal-decorators", { 
            "legacy": true 
        }], 
        ["@babel/plugin-proposal-class-properties", { 
            "loose": true 
        }] 
    ] 
}

装饰器有什么优点

为什么要使用 Decorator,其实就是介绍到 AOP 范式的最大特点了:非侵入式增强。

非侵入式增强,顾名思义,不会对原代码有侵入性。比如我们想做一个通用的组件,这个组件会提供一些基础的功能,我想在这个组件的基础上做一些改造和增强,装饰器模型就是一个很好的选择

此时有的人就会问了:“你这个场景,用继承也可以啊”

确实,但是继承会导致类与类之间耦合度较高,而装饰器相比生成子类更为灵活

缺点

兼容性是真的不行,使用的话还需安装配置babel插件,麻烦

装饰器的原理

Object.defineProperty

Decorator其实就是一个语法糖,背后其实就是利用es5的Object.defineProperty(target,name,descriptor)

其中最核心的其实是 descriptor —— 属性描述符

属性描述符总共分两种:数据描述符(Data descriptor)和 访问器描述符(Accessor descriptor)。

描述符必须是两种形式之一,但不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

  • configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

  • enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

  • value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

  • writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值:

  • get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。

  • set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。

如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常

描述符的api使用

数据描述符:

Object.getOwnPropertyDescriptor(user,'name');

// 输出
/**
{
  "value": "张三",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
**/

访问器描述符:

var anim = { 
  get age() { return 5; } 
};
Object.getOwnPropertyDescriptor(anim, "age");
// 输出
/**
{
   configurable: true,
   enumerable: true,
   get: /*the getter function*/,
   set: undefined
 }
**/

伪代码实现

举个🌰,我们要实现一个Person类:

class Person{
  say(){
    console.log('哈哈,前端真简单!');
  }
}

执行上面的代码,大致代码如下:

Object.defineProperty(Person.prototype,'say',{
  value:function(){console.log('哈哈,前端真简单!')},
  enumerable:false,
  configurable:true,
  writable:true
})

如果我们利用装饰器来修饰他

class Person{
@readonly
say(){console.log('感觉头有点凉')}
}

在这种装饰器的属性,会在Object.defineProperty为Person.prototype注册say属性之前,执行以下代码:

let descriptor = {
  value:specifiedFunction,
  enumerable:false,
  configurable:true,
  writeable:true
};

descriptor = readonly(Person.prototype,'say',descriptor)||descriptor;
Object.defineProperty(Person.prototype,'say',descriptor);

从上面的伪代码我们可以看出,Decorator只是在Object.defineProperty为Person.prototype注册属性之前,执行了一个装饰函数,其属于一个类对Object.defineProperty的拦截。所以它和Object.defineProperty具有一致的形参:

  • obj:作用的目标对象

  • prop:作用的属性名

  • descriptor:针对该属性的描述符

如何使用一个装饰器

看到这里,相信有的读者会产生疑问:上面的伪代码的实现,readonly是一个怎样的函数呢?我怎么做才能自己写一个可以使用的装饰器?

一般来说,装饰器有两种:类装饰器、类属性装饰器

使用类装饰器

话不多说,直接给示例: 1:

const withSpeak = (constructor) => {
    const prototype = constructor.prototype;
    prototype.speak = function() {
        console.log('I can speak ', this.language);
    }
}
@withSpeak
class Student {
    constructor(language) {
        this.language = language;
    }
}
const student1 = new Student('Chinese');
const student2 = new Student('English');
student1.speak(); // I can speak  Chinese

student2.speak(); // I can speak  Englist

2:

@name
@seal
class Person {
  sayHello() {
    console.log(`hello ,my name is ${this.name}`)
  }
}

function name(constructor) {
  Object.defineProperty(constructor.prototype,'name',{
    value:'小明'
  })
}

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
  Object.defineProperty(constructor.prototype, 'sayHello', {
    ...descriptor,
    writable: false
  })
}

new Person().sayHello()
new Person().sayHello = 1;// Cannot assign to read only property 'sayHello' of object '#<Person>'

装饰函数传入你要修饰的类的constructor,拿到constructor后可以在类的原型上进行修改,也可以利用Object.getOwnPropertyDescriptor、Object.defineProperty修改类的描述

使用类属性装饰器

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
class Person {
    @readonly name = 'person'
}
const person = new Person();
person.name = 'tom'; 

亦或者是mobx

import {
    observable,
    action,
    autorun
} from 'mobx'

class Store {
    @observable count = 1;
    @action
    changeCount(count) {
        this.count = count;
    }
}

const store = new Store();
autorun(() => {
    console.log('count is ', store.count);
})
store.changeCount(10); // 修改 count 的值,会引起 autorun 中的函数自动执行。

类属性装饰器可以用在类的属性、方法、get/set 函数中,装饰函数一般会接收三个参数:

  • target:被修饰的类
  • name:类成员的名字
  • descriptor:属性描述符,对象会将这个参数传给 Object.defineProperty

babel是怎么实现一个装饰器的

未完待续。。。

参考文章

mp.weixin.qq.com/s/oAvP9YI-7…

mp.weixin.qq.com/s/5BRWVsMZ4…

juejin.cn/post/684490…