边学边译JS机制--38.js 中的decorator

271 阅读2分钟

Introduction

装饰器(Decorators)是用一个装饰器函数,把另一块代码(比如类或者函数)包起来一个东西。它的目的就是扩展被包含的代码,而不用去修改它。

正JAVA,C#,python中,这种设计模式已经流行了很长一段时间了。

比如Python中可以这么使用:

@myFnDecorator
def decoratedFn():
    print("Decorated hello world!")

前缀**@** 告诉解析器,这是一个给装饰器。 myFnDecorator 则是一个装饰器函数的名字。

myFnDecorator 有一个参数---decoratedFn ,然后返回扩展后的decoratedFn 函数 上面的例子可以看到,python中装饰器是一个高级函数,可以把一个函数当作参数传进来

@function-name语法则提供了一个方便的方式,来调用高级函数。 JS中的装饰器,跟它很类似。

JS中的装饰器

JS中,装饰器还是一个第二阶段的提议 stage 2 proposal ,但是通过Babel使用,也能直接在TS中使用。 它同样是一个高级函数,可以接受函数参数,类参数等,然后给参数扩展一些功能,而不用修改参数里面的内容。 看一个简单的例子:

class Person {
   @decoratorFn
    getPin() {
        return `my PIN is ${this.pin}`
    }
}

@decoratorFn 表示的式一个高级函数,也就是我们的装饰器函数,它在运行时会把getPin作为参数。

同样,我们看到装饰器提供了显式的语法来出发高等级函数。 这让我们的代码更干净,并且易于复用。

由于装饰器可以不会修改被包含的代码,没有侵入性。因此经常用来处理高级函数和函数组合。

在JS中,我们可以命令式的装饰一块代码:

function filterEvenNumbers (list) {
    if(!list.length) {
        throw new Error ("Array should not be empty!")
    }
    return list.filter((listItem) => {
        if(!Number.isInteger(listItem)) {
            throw new Error("Array should contain only integers!")
        }
        return listItem % 2 === 0
    })
}

 filterEvenNumbers函数遍历一个数组中的所有整数,然后返回了新的平滑数组。我们可以使用装饰器给他增加一些异常处理

装饰器函数是这样:

function handleError (func1) {
    return (list) => {
        try {
            const result = func1(list)
            return result;
        }catch(error) {
            console.log("Error is:", error.message)
        }
    }
}

handleError(filterEvenNumbers)([1,2,3,4]) // returns [2,4]
handleError(filterEvenNumbers)([]) // prints Error is: Array should not be empty!
handleError(filterEvenNumbers)([2,3.1]) 
// prints Error is: Array should contain only integers!

handleError 是一个高级函数,参数是一个函数,在 try...catch 包含的代码中执行。如果执行时发生了异常,handleError 捕捉它然后在控制台打印。

装饰器就是按照这种方式,来给你的代码增加功能。

React 的高阶组件和Redux的connect函数,都是使用了这种模式。 在JS中我们可以使用装饰器装饰自己的代码,但是为什么需要装饰器呢?

为什么需要装饰器

尽管我们可以继承ES6的类,但是我们需要更好的方式在多个类之间分享它们的功能,这种方式更干净,更好。 装饰器提供了声明式语法,标记和修改JS的类,属性和对象常量。

有一些库,比如 core-decorators 提供了一系列的装饰器,来装饰类或者类的成员。 还有一些其他的库:

这些装饰器,只需要简单的导入进来就行:

import { autobind } from 'core-decorators';

class Vehicle {
  @autobind // binds the this variable to an instance of Vehicle
  getVehicle() {
      return this;
  }
}

let car = new Vehicle(); // creates an instance of Vehicle called car
let { getVehicle } = car;

getVehicle() === car; // returns true;

这里我们看到 @autobind 装饰器是绑定了getVehicle 方法的this 变量,它指向 car 实例。 我们深入看一下装饰器

解剖装饰器

如上所述,JS装饰器在名字前有一个 @ 符号:

class Person {
  @readonly
  getName() { return ${this.first} ${this.last} }
}

@readonly 表达式会返回一个装饰器函数,这个函数有三个参数:target, name, and descriptor.

target参数指向被装饰的对象,name参数指向target的名字,descriptor参数指向target的装饰器对象。

因为 @readonly 返回一个函数,我们可以这么使用 @readonly(args)

也可以组合装饰器:

function a(param) {
    return (target) => {
        // do something with param
    }
}

function b(target) {
   // do something
}

@a("something")
@b
class Person {
  // some code
}

也可以在类成员上使用装饰器:

function a(param) {
    return (target) => {
        // do something with param
    }
}

function b(target) {
   // do something
}

class Person {
  @a("something")
  @b
  getName() {}
}

JS装饰器的工作方式

descriptor 对象属性

JS 对象是很多键值对组成的。每一个属性都有一个descriptor 属性,它是一个可变对象。descriptor 属性包含了属性值和其他属性的元数据描述。 假如有这么一段代码:

const Car = {name: "Rav4", type: "SUV", plateNo: 12345}

Object.getOwnPropertyDescriptor(Car, 'name');
// returns 
{ value: 'Rav4', writable: true, enumerable: true, configurable: true }

在上面的代码中,我们使用[ Object.getOwnPropertyDescriptor() ] 方法去Car对象的name属性的获取descriptor 属性对象。 descriptor 对象中的 writableenumerable和 configurable 属性决定name 属性的行为。

它确定name 属性是否可写,可配置,或者可枚举的。 同样,我们可以使用[ Object.defineProperty() ]自定义一个descriptor ,来创建一个新的属性或者更新一个已有的属性

考虑一下面的代码:

const Car = { name: "Rav4", type: "SUV", plateNo: 12345 };

Object.defineProperty(Car, "name", {
  writable: false
});

let carProp = Object.getOwnPropertyDescriptor(Car, "name");

console.log(carProp); // prints
{value: "Rav4", writable: false, enumerable: true, configurable: true}

我们修改了 name 属性的 descriptor对象的 writable属性,让 name 不可被编辑。 这样当你想改变name 属性,就会有下面的报错

const Car = { name: "Rav4", type: "SUV", plateNo: 12345 };
Object.defineProperty(Car, "name", {
  writable: false
});

console.log((Car.name = "Venza")); // prints
// TypeError: Cannot assign to read only property 'name' of object '#<Object>'

现在,我们知道如何去装饰属性了。

装饰属性

假如有下面的代码:

class Car {
    getPlateNO() {`My plate number is ${this.platNO}`}
}

假如我们在Car.prototype上加上一个getPlateNO 方法,可能会是这样子的:

Object.defineProperty(Car.prototype, 'getPlateNO', {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
});

specifiedFunction 指向getPlateNO 属性的值,也就是getPlateNO  方法本身。

如果我们想让getPlateNO 只读,我们可以创建一个 @readonly 装饰器:

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

然后使用它:

class Car {
    @readonly
    getPlateNO() {`My plate number is ${this.plateNO}`}
}

在给Car.prototype安装 descriptor 之前,JS引擎首先触发装饰器。在响应的defineProperty触发之前,这是一个代请求的时机。

像下面这样:

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

descriptor = readonly(Car.prototype, 'getPlateN0', descriptor) || descriptor;
Object.defineProperty(Car.prototype, 'getPlateN0', descriptor);

上面代码中,readonly装饰器被descriptor 对象作为第三个参数调用。它修改了这个descriptor,并返回了一个自定义的descriptor 对象,这个对象的writable属性被设置为false.

自定义的 descriptor对象现在传递给Object.defineProperty 方法,去修改getPlateNO方法的descriptor  属性。成功的让getPlateNO 方法只读。

这种装饰模式非常干净,易用,易懂。

装饰器类型

类装饰器

我们也可以像这样装饰一个类:

function annotation(target) {
   // Add a property on target
   target.isAnnotated = true;
}

// A simple decorator
@annotation
class Book {
      // some code 
 }

console.log(Book.isAnnotated) // prints true

上面的代码被翻译为:

"use strict";
var _class;
function annotation(target) {
  // Add a property on target
  target.isAnnotated = true;
}
let Book = annotation(
    (_class = class Book { 
        // some code 
    })) || _class;
console.log(Book.isAnnotated); // prints true

使用 annotation 装饰器很容易就给Book 类添加了一个标注版本。 我们知道 @annotation返回一个函数,那我们还可以给它传递参数:

function annotation (published) {
    return (target) => {
        // Add a property on target
        target.isAnnotated = true;
        target.isPublished = published;
    }
}
@annotation(true)
class Book {}
console.log(Book.isPublished) // prints true

等同于:

"use strict";
var _dec, _class;
function annotation(published) {
  return (target) => {
    // Add a property on target
    target.isAnnotated = true;
    target.isPublished = published;
  };
}
let Book =
  ((_dec = annotation(true)), _dec((_class = class Book {})) || _class);
console.log(Book.isPublished); // prints true

通过给装饰器传递参数,我们可以控制 Book 是否是published的。

我们的装饰器函数扮演了一个装饰器工厂,返回一个被装饰器在运行时调用的表达式。 这一点可以用在属性装饰器上。

类装饰器的成员

假如有这样一段代码:

function logDescription (descrition) {
    return (target, key, descriptor) => {
        let fn = descriptor.value;
        descriptor.value = (...args) => {
          console.log(`The following payment is for ${descrition}`);
          return fn.call(this, ...args);
        }
        
        return descriptor;
    }
}

我们使用descriptor.value 访问类的方法,这得益于target上的 descriptor  操作。 我们用自定义的方法替换它,这个自定义方法是基于我们给装饰器函数传递的参数的,会打印payment描述。

我们可以这么使用:

class Person {
    @logDescription("School fees")
    makePayment(currency,amount) {
        console.log(`${currency}${amount} has been debited from your account`)
    }
}
const father = new Person();
console.log(father.makePayment("$", 1000)); // prints
// The following payment is for School fees
// $1000 has been debited from your account

综述

装饰器非常强大,用处很广。 简单的一行代码,就可以我们的函数变成一个REST终结点,打印有用信息等等。 它让你的代码干净,易读,并且DRY(Dont Repeat youself)。 增加一个抽象的附加增,会让代码复用性更好,但是也带来一个问题---调试的难度增大了。但这个问题对于装饰器来说就不那么明显了。