logo

[译] javascript 装饰器

原文地址

概述

装饰器(decorator)让我们在设计创建类时修改类及其属性成为可能。
在 ES5 中字面量对象的值可以为任意的表达式,而在 ES6 的 Class 中只支持字面量函数作为 Class 的值。
装饰器恢复了设计创建类时运行代码的能力,同时保持一种声明式的语法。

详细设计

一个装饰器是:

  • 一个表达式
  • 其等同于一个函数
  • 其接受目标对象,目标对象属性名,装饰器属性描述符(descriptor)作为参数
  • 其可以选择返回装饰器属性描述符用于修改目标对象

对一个简单的类定义进行思考:

1
2
3
class Person(){
name(){return `${this.first} ${this.last}`}
}

这个类声明执行时,将 name 函数添加到 Person.prototype 上的过程大致如下:

1
2
3
4
5
6
Object.defineProperty(Person.prototype, "name", {
value: secifiedFunction,
enumerable: false,
configurable: true,
writable: true
});

一个装饰器在定义一个属性之前执行:

1
2
3
4
5
6
class Person {
@readonly
name() {
return `${this.first} ${this.last}`;
}
}

现在,在使用属性修饰符定义Person.prototype属性之前,javascaript 引擎会先执行装饰器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let description = {
type: "method",
initializer: () => specifiedFunction,
enumerable: false,
configurable: true,
writable: true
};

description = readonly(Person.prototype, "name", description) || description;
defineDecoratedProperty(Person.prototype, "name", description);

function defineDecoratedProperty(
target,
key,
{ initializer, enumerable, configurable, writable }
) {
Object.defineProperty(target, key, {
value: initializer(),
enumerable,
configurable,
writable
});
}

在相关defineProperty实际执行之前有机会对属性描述符进行调整。
一个装饰器在语法上先于存取器的getter/setter执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
@nonenumerable
get kidCount() {
return this.children.length;
}
}

let description = {
type: "accessor",
get: specifiedGetter,
enumerable: true,
configurable: true
};

function nonenumerable(target, name, descriptor) {
description.enumerable = false;
return description;
}

这里有个更详细例子,用一个简单的装饰器记录对象属性的存取器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Person {
@memoize
get name() {
return `${this.first} ${this.last}`;
}
set name(val) {
let [first, last] = val.split(" ");
this.first = first;
this.last = last;
}
}
/*
记录存取器的值,descriptor与记录对象table一一对应
*/
let memoized = new WeakMap();
/*
拦截并修改存取器方法,便于存储存取器的操作
*/
function memoize(target, name, descriptor) {
let getter = descriptor.get,
setter = descriptor.set;

descriptor.get = function() {
let table = memoizationFor(this);
if (name in table) {
return table[name];
}
return (table[name] = getter.call(this));
};

descriptor.set = function(val) {
let table = memoizationFor(this);
setter.call(this, val);
table[name] = val;
};
}

/*
判断对应的descriptor是否有存储的table记录
没有则创建table,存储table并返回
*/
function memoizationFor(obj) {
let table = memoized.get(obj);
if (!table) {
table = Object.create(null);
memoized.set(obj, table);
}
return table;
}

装饰器也能装饰类自身,此时装饰器能够获取目标类的构造函数(constructor)(类自身)。

1
2
3
4
5
6
7
8
9
10
@annotation
class MyClass {}

/*
对MyClass构造函数添加annoteated属性
*/
function annotation(target) {
// Add a property on target
target.annotated = true;
}

因为装饰器是表达式,装饰器可以接受额外的参数,这是装饰器就像一个工厂函数。

1
2
3
4
5
6
7
8
@isTestable(true)
class MyClass {}

function isTestable(value) {
return function decorator(target) {
target.isTestable = value;
};
}

同样的,这项技巧也可用于属性装饰器(property decorator):

1
2
3
4
5
6
7
8
9
10
11
class C {
@enumerable(false)
method() {}
}

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

由于装饰器对目标对象进行操作,因此装饰器自然对静态方法也有效。唯一不同的的是装饰器接收的第一个 target 参数是构造函数(类自身)而不是其原型(prototype),因为静态方法是通过defineProperty定义在构造函数上的(而不是构造函数原型)。
同样的,装饰器同样可以作用于字面量对象的属性,向装饰器函数传递已创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
@getValue
b: 2
};

function getValue(target, key, des) {
console.log("is getting b");
let val = des.value;
return des;
}

console.log(obj.b);
//is getting b
//2

语法糖

类声明

语法

1
2
3
@F("color")
@G
class Foo {}

等效代码(ES6)

1
2
3
4
5
6
var Foo = (function() {
class Foo {}
/* 修饰器由内而外依次执行 */
Foo = F("color")((Foo = G(Foo) || Foo)) || Foo;
return Foo;
})();

等效代码(ES5)

1
2
3
4
5
6
var Foo = (function() {
function Foo() {}

Foo = F("color")((Foo = G(Foo) || Foo)) || Foo;
return Foo;
})();

类方法声明

语法

1
2
3
4
5
class Foo {
@F("color")
@G
bar() {}
}

等效代码(ES6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var Foo = (function() {
class Foo {
bar() {}
}

var _temp;
_temp =
F("color")(
Foo.prototype,
"bar",
(_temp =
G(
Foo.prototype,
"bar",
(_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar"))
) || _temp)
) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

等效代码(ES5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Foo = (function() {
function Foo() {}
Foo.prototype.bar = function() {};

var _temp;
_temp =
F("color")(
Foo.prototype,
"bar",
(_temp =
G(
Foo.prototype,
"bar",
(_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar"))
) || _temp)
) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

类的存取器声明

语法

1
2
3
4
5
6
class Foo {
@F("color")
@G
get bar() {}
set bar(value) {}
}

等效代码(ES6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Foo = (function() {
class Foo {
get bar() {}
set bar(value) {}
}

var _temp;
_temp =
F("color")(
Foo.prototype,
"bar",
(_temp =
G(
Foo.prototype,
"bar",
(_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar"))
) || _temp)
) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

等效代码(ES5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Foo = (function() {
function Foo() {}
Object.defineProperty(Foo.prototype, "bar", {
get: function() {},
set: function(value) {},
enumerable: true,
configurable: true
});

var _temp;
_temp =
F("color")(
Foo.prototype,
"bar",
(_temp =
G(
Foo.prototype,
"bar",
(_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar"))
) || _temp)
) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

字面量对象方法声明

语法

1
2
3
4
5
var o = {
@F("color")
@G
bar() {}
};

等效代码(ES6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var o = (function() {
var _obj = {
bar() {}
};

var _temp;
_temp =
F("color")(
_obj,
"bar",
(_temp = G(_obj, "bar", (_temp = void 0)) || _temp)
) || _temp;
if (_temp) Object.defineProperty(_obj, "bar", _temp);
return _obj;
})();

等效代码(ES5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var o = (function() {
var _obj = {
bar: function() {}
};

var _temp;
_temp =
F("color")(
_obj,
"bar",
(_temp = G(_obj, "bar", (_temp = void 0)) || _temp)
) || _temp;
if (_temp) Object.defineProperty(_obj, "bar", _temp);
return _obj;
})();

字面量对象存取器声明

语法

1
2
3
4
5
6
var o = {
@F("color")
@G
get bar() { }
set bar(value) { }
}

等效语法(ES6)

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = (function () {
var _obj = {
get bar() { }
set bar(value) { }
}

var _temp;
_temp = F("color")(_obj, "bar",
_temp = G(_obj, "bar",
_temp = void 0) || _temp) || _temp;
if (_temp) Object.defineProperty(_obj, "bar", _temp);
return _obj;
})();

等效语法(ES5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var o = (function() {
var _obj = {};
Object.defineProperty(_obj, "bar", {
get: function() {},
set: function(value) {},
enumerable: true,
configurable: true
});

var _temp;
_temp =
F("color")(
_obj,
"bar",
(_temp = G(_obj, "bar", (_temp = void 0)) || _temp)
) || _temp;
if (_temp) Object.defineProperty(_obj, "bar", _temp);
return _obj;
})();

语法

  DecoratorList [Yield] :

   DecoratorList [?Yield]optDecorator [?Yield]


  Decorator [Yield] :

   @LeftHandSideExpression [?Yield]


  PropertyDefinition [Yield] :

   IdentifierReference [?Yield]

   CoverInitializedName [?Yield]

   PropertyName [?Yield]:AssignmentExpression [In, ?Yield]

   DecoratorList [?Yield]optMethodDefinition [?Yield]


  CoverMemberExpressionSquareBracketsAndComputedPropertyName [Yield] :

   [Expression [In, ?Yield]]


NOTE The production CoverMemberExpressionSquareBracketsAndComputedPropertyName is used to cover parsing a MemberExpression that is part of a Decorator inside of an ObjectLiteral or ClassBody, to avoid lookahead when parsing a decorator against a ComputedPropertyName.


  PropertyName [Yield, GeneratorParameter] :

   LiteralPropertyName

   [+GeneratorParameter] CoverMemberExpressionSquareBracketsAndComputedPropertyName

   [~GeneratorParameter] CoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]


  MemberExpression [Yield]  :

   [Lexical goal InputElementRegExp] PrimaryExpression [?Yield]

   MemberExpression [?Yield]CoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]

   MemberExpression [?Yield].IdentifierName

   MemberExpression [?Yield]TemplateLiteral [?Yield]

   SuperProperty [?Yield]

   NewSuperArguments [?Yield]

   newMemberExpression [?Yield]Arguments [?Yield]


  SuperProperty [Yield] :

   superCoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]

   super.IdentifierName


  ClassDeclaration [Yield, Default] :

   DecoratorList [?Yield]optclassBindingIdentifier [?Yield]ClassTail [?Yield]

   [+Default] DecoratorList [?Yield]optclassClassTail [?Yield]


  ClassExpression [Yield, GeneratorParameter] :

   DecoratorList [?Yield]optclassBindingIdentifier [?Yield]optClassTail [?Yield, ?GeneratorParameter]


  ClassElement [Yield] :

   DecoratorList [?Yield]optMethodDefinition [?Yield]

   DecoratorList [?Yield]optstaticMethodDefinition [?Yield]

备注

支持仅限元数据的装饰器,这是静态分析所需要的特性。TypeScript 项目让用户通过受限制的语法自定义装饰器环境进而实现不求值也能完成正确的静态分析。