Javascript中装饰器的实现原理

4,272 阅读3分钟

基于Node的web服务器开发中使用decorator对请求进行权限校验和数据格式的处理是一个看起来比较漂亮的写法,这里正好整理一下对javascript中的decorator的理解。

decorator的概念在其他语言中早有存在,在javascript中目前(2017/09/17)还处于stage 2阶段,基本确定会进入正式的ECMA规范了。但是目前还不能直接使用,只能使用babel来进行语法的转换。

官方的说法是:

Decorators make it possible to annotate and modify classes and properties at design time.

大概的意思就是在运行时改变类或者类的属性。

正文

首先看下装饰器常见的两种使用:

  1. 装饰一个类的属性。
function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}
class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()    // meow ~
  1. 装饰一个类。
function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true

装饰一个类的属性

ES6中的类实际上就是一个语法糖,本质上是构造函数,类的属性的定义使用的是 Object.defineProperty() 用一个简单的栗子来理解如下:

class Cat {
    say() {
        console.log("meow ~");
    }
}

function Cat() {}
Object.defineProperty(Cat.prototype, "say", {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
});

细心的小伙伴已经发现了

Object.defineProperty(obj, prop, descriptor)

接收的参数和作用于类的属性的时候装饰器函数的接收的参数很像。

可以知道作用于类的属性的时候的装饰器函数接收的参数就是上述ES6中的类定义属性时候使用Object.defineProperty时接收的参数,一模一样...

本质上也就是说装饰器在作用于类的属性的时候,实际上是通过 Object.defineProperty 来对原有的descriptor进行封装:

descriptor:

  • configurable控制是不是能删、能修改descriptor本身。
  • writable控制是不是能修改值。
  • enumerable控制是不是能枚举出属性。
  • value控制对应的值,方法只是一个value是函数的属性。
  • get和set控制访问的读和写逻辑。

通过处理descriptor可以改变原有属性。 被装饰的属性的定义在实际上执行的是以下的代码:

let descriptor = {
    value: function() {
        console.log("meow ~");
    },
    enumerable: false,
    configurable: true,
    writable: true
};
descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;
Object.defineProperty(Cat.prototype, "say", descriptor);

也就是说,上面的那个@readonly其实就是

descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;

的语法糖,要注意的是,装饰器执行的时间是在属性定义的时候,也就是被装饰的属性在定义后就是已经被装饰器处理过的不一样的属性了。

装饰一个类

装饰一个类的时候类本身本质上是一个函数,没有descriptor,target是这个函数本身。

function isAnimal(target) {
    target.isAnimal = true;
  	return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true

也就是说,上面的@isAnimal其实就是做了下面这件事

Cat = isAnimal(function Cat() { ... });

在了解了两种情况下装饰器本质上做了什么之后,顺带可以看出,装饰器函数执行的时间:

function log(message) {
    return function() {
        console.log(message);
    }
}
console.log('before class');
@log('class Bar')
class Bar {
    @log('class method bar');
    bar() {}
    @log('class getter alice');
    get alice() {}
    @log('class property bob');
    bob = 1;
}
console.log('after class');
let bar = {
    @log('object method bar')
    bar() {}
};

输出结果:

before class
class method bar
class getter alice
class property bob
class Bar
after class
object method bar

可以看出装饰器在定义时就执行了,也就对应着官方的那句话:

Decorators make it possible to annotate and modify classes and properties at design time.

在类和类的属性定义的时候就对它们进行了"装饰"。

以上大致的说了下javascript的装饰器的原理和使用,但是还有一些细节有待进一步的深入。

TBD

参考资料