Decorator

2020-06-11 15:39 更新

簡介

Decorator 提案經(jīng)過了大幅修改,目前還沒有定案,不知道語法會不會再變。下面的內(nèi)容完全依據(jù)以前的提案,已經(jīng)有點過時了。等待定案以后,需要完全重寫。

裝飾器(Decorator)是一種與類(class)相關(guān)的語法,用來注釋或修改類和類方法。許多面向?qū)ο蟮恼Z言都有這項功能,目前有一個提案將其引入了 ECMAScript。

裝飾器是一種函數(shù),寫成@ + 函數(shù)名。它可以放在類和類方法的定義前面。

  1. @frozen class Foo {
  2. @configurable(false)
  3. @enumerable(true)
  4. method() {}
  5. @throttle(500)
  6. expensiveMethod() {}
  7. }

上面代碼一共使用了四個裝飾器,一個用在類本身,另外三個用在類方法。它們不僅增加了代碼的可讀性,清晰地表達(dá)了意圖,而且提供一種方便的手段,增加或修改類的功能。

1. 類的裝飾

裝飾器可以用來裝飾整個類。

  1. @testable
  2. class MyTestableClass {
  3. // ...
  4. }
  5. function testable(target) {
  6. target.isTestable = true;
  7. }
  8. MyTestableClass.isTestable // true

上面代碼中,@testable 就是一個裝飾器。它修改了 MyTestableClass 這個類的行為,為它加上了靜態(tài)屬性 isTestable 。testable 函數(shù)的參數(shù)targetMyTestableClass 類本身。

基本上,裝飾器的行為就是下面這樣。

  1. @decorator
  2. class A {}
  3. // 等同于
  4. class A {}
  5. A = decorator(A) || A;

也就是說,裝飾器是一個對類進(jìn)行處理的函數(shù)。裝飾器函數(shù)的第一個參數(shù),就是所要裝飾的目標(biāo)類。

  1. function testable(target) {
  2. // ...
  3. }

上面代碼中, testable 函數(shù)的參數(shù) target ,就是會被裝飾的類。

如果覺得一個參數(shù)不夠用,可以在裝飾器外面再封裝一層函數(shù)。

  1. function testable(isTestable) {
  2. return function(target) {
  3. target.isTestable = isTestable;
  4. }
  5. }
  6. @testable(true)
  7. class MyTestableClass {}
  8. MyTestableClass.isTestable // true
  9. @testable(false)
  10. class MyClass {}
  11. MyClass.isTestable // false

上面代碼中,裝飾器 testable 可以接受參數(shù),這就等于可以修改裝飾器的行為。

注意,裝飾器對類的行為的改變,是代碼編譯時發(fā)生的,而不是在運行時。這意味著,裝飾器能在編譯階段運行代碼。也就是說,裝飾器本質(zhì)就是編譯時執(zhí)行的函數(shù)。

前面的例子是為類添加一個靜態(tài)屬性,如果想添加實例屬性,可以通過目標(biāo)類的 prototype 對象操作。

  1. function testable(target) {
  2. target.prototype.isTestable = true;
  3. }
  4. @testable
  5. class MyTestableClass {}
  6. let obj = new MyTestableClass();
  7. obj.isTestable // true

上面代碼中,裝飾器函數(shù) testable 是在目標(biāo)類的 prototype 對象上添加屬性,因此就可以在實例上調(diào)用。

下面是另外一個例子。

  1. // mixins.js
  2. export function mixins(...list) {
  3. return function (target) {
  4. Object.assign(target.prototype, ...list)
  5. }
  6. }
  7. // main.js
  8. import { mixins } from './mixins'
  9. const Foo = {
  10. foo() { console.log('foo') }
  11. };
  12. @mixins(Foo)
  13. class MyClass {}
  14. let obj = new MyClass();
  15. obj.foo() // 'foo'

上面代碼通過裝飾器 mixins ,把 Foo 對象的方法添加到了 MyClass 的實例上面??梢杂?Object.assign()模擬這個功能。

  1. const Foo = {
  2. foo() { console.log('foo') }
  3. };
  4. class MyClass {}
  5. Object.assign(MyClass.prototype, Foo);
  6. let obj = new MyClass();
  7. obj.foo() // 'foo'

實際開發(fā)中,React 與 Redux 庫結(jié)合使用時,常常需要寫成下面這樣。

  1. class MyReactComponent extends React.Component {}
  2. export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了裝飾器,就可以改寫上面的代碼。

  1. @connect(mapStateToProps, mapDispatchToProps)
  2. export default class MyReactComponent extends React.Component {}

相對來說,后一種寫法看上去更容易理解。

2. 方法的裝飾

裝飾器不僅可以裝飾類,還可以裝飾類的屬性。

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

上面代碼中,裝飾器 readonly用來裝飾“類”的 name 方法。

裝飾器函數(shù)readonly 一共可以接受三個參數(shù)。

  1. function readonly(target, name, descriptor){
  2. // descriptor對象原來的值如下
  3. // {
  4. // value: specifiedFunction,
  5. // enumerable: false,
  6. // configurable: true,
  7. // writable: true
  8. // };
  9. descriptor.writable = false;
  10. return descriptor;
  11. }
  12. readonly(Person.prototype, 'name', descriptor);
  13. // 類似于
  14. Object.defineProperty(Person.prototype, 'name', descriptor);

裝飾器第一個參數(shù)是類的原型對象,上例是 Person.prototype ,裝飾器的本意是要“裝飾”類的實例,但是這個時候?qū)嵗€沒生成,所以只能去裝飾原型(這不同于類的裝飾,那種情況時 target 參數(shù)指的是類本身);第二個參數(shù)是所要裝飾的屬性名,第三個參數(shù)是該屬性的描述對象。

另外,上面代碼說明,裝飾器(readonly)會修改屬性的描述對象(descriptor),然后被修改的描述對象再用來定義屬性。

下面是另一個例子,修改屬性描述對象的 enumerable 屬性,使得該屬性不可遍歷。

  1. class Person {
  2. @nonenumerable
  3. get kidCount() { return this.children.length; }
  4. }
  5. function nonenumerable(target, name, descriptor) {
  6. descriptor.enumerable = false;
  7. return descriptor;
  8. }

下面的@log裝飾器,可以起到輸出日志的作用。

  1. class Math {
  2. @log
  3. add(a, b) {
  4. return a + b;
  5. }
  6. }
  7. function log(target, name, descriptor) {
  8. var oldValue = descriptor.value;
  9. descriptor.value = function() {
  10. console.log( Calling ${name} with , arguments);
  11. return oldValue.apply(this, arguments);
  12. };
  13. return descriptor;
  14. }
  15. const math = new Math();
  16. // passed parameters should get logged now
  17. math.add(2, 4);

上面代碼中,@log 裝飾器的作用就是在執(zhí)行原始的操作之前,執(zhí)行一次console.log,從而達(dá)到輸出日志的目的。

裝飾器有注釋的作用。

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

從上面代碼中,我們一眼就能看出, Person 類是可測試的,而 name 方法是只讀和不可枚舉的。

下面是使用 Decorator 寫法的組件,看上去一目了然。

  1. @Component({
  2. tag: 'my-component',
  3. styleUrl: 'my-component.scss'
  4. })
  5. export class MyComponent {
  6. @Prop() first: string;
  7. @Prop() last: string;
  8. @State() isVisible: boolean = true;
  9. render() {
  10. return (
  11. <p>Hello, my name is {this.first} {this.last}</p>
  12. );
  13. }
  14. }

如果同一個方法有多個裝飾器,會像剝洋蔥一樣,先從外到內(nèi)進(jìn)入,然后由內(nèi)向外執(zhí)行。

  1. function dec(id){
  2. console.log('evaluated', id);
  3. return (target, property, descriptor) => console.log('executed', id);
  4. }
  5. class Example {
  6. @dec(1)
  7. @dec(2)
  8. method(){}
  9. }
  10. // evaluated 1
  11. // evaluated 2
  12. // executed 2
  13. // executed 1

上面代碼中,外層裝飾器@dec(1)先進(jìn)入,但是內(nèi)層裝飾器@dec(2)先執(zhí)行。

除了注釋,裝飾器還能用來類型檢查。所以,對于類來說,這項功能相當(dāng)有用。從長期來看,它將是 JavaScript 代碼靜態(tài)分析的重要工具。

3. 為什么裝飾器不能用于函數(shù)?

裝飾器只能用于類和類的方法,不能用于函數(shù),因為存在函數(shù)提升。

  1. var counter = 0;
  2. var add = function () {
  3. counter++;
  4. };
  5. @add
  6. function foo() {
  7. }

上面的代碼,意圖是執(zhí)行后 counter 等于 1,但是實際上結(jié)果是 counter 等于 0。因為函數(shù)提升,使得實際執(zhí)行的代碼是下面這樣。

  1. @add
  2. function foo() {
  3. }
  4. var counter;
  5. var add;
  6. counter = 0;
  7. add = function () {
  8. counter++;
  9. };

下面是另一個例子。

  1. var readOnly = require("some-decorator");
  2. @readOnly
  3. function foo() {
  4. }

上面代碼也有問題,因為實際執(zhí)行是下面這樣。

  1. var readOnly;
  2. @readOnly
  3. function foo() {
  4. }
  5. readOnly = require("some-decorator");

總之,由于存在函數(shù)提升,使得裝飾器不能用于函數(shù)。類是不會提升的,所以就沒有這方面的問題。

另一方面,如果一定要裝飾函數(shù),可以采用高階函數(shù)的形式直接執(zhí)行。

  1. function doSomething(name) {
  2. console.log('Hello, ' + name);
  3. }
  4. function loggingDecorator(wrapped) {
  5. return function() {
  6. console.log('Starting');
  7. const result = wrapped.apply(this, arguments);
  8. console.log('Finished');
  9. return result;
  10. }
  11. }
  12. const wrapped = loggingDecorator(doSomething);

4. core-decorators.js

core-decorators.js是一個第三方模塊,提供了幾個常見的裝飾器,通過它可以更好地理解裝飾器。

(1)@autobind

autobind裝飾器使得方法中的 this對象,綁定原始對象。

  1. import { autobind } from 'core-decorators';
  2. class Person {
  3. @autobind
  4. getPerson() {
  5. return this;
  6. }
  7. }
  8. let person = new Person();
  9. let getPerson = person.getPerson;
  10. getPerson() === person;
  11. // true

(2)@readonly

readonly 裝飾器使得屬性或方法不可寫。

  1. import { readonly } from 'core-decorators';
  2. class Meal {
  3. @readonly
  4. entree = 'steak';
  5. }
  6. var dinner = new Meal();
  7. dinner.entree = 'salmon';
  8. // Cannot assign to read only property 'entree' of [object Object]

(3)@override

override裝飾器檢查子類的方法,是否正確覆蓋了父類的同名方法,如果不正確會報錯。

  1. import { override } from 'core-decorators';
  2. class Parent {
  3. speak(first, second) {}
  4. }
  5. class Child extends Parent {
  6. @override
  7. speak() {}
  8. // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
  9. }
  10. // or
  11. class Child extends Parent {
  12. @override
  13. speaks() {}
  14. // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  15. //
  16. // Did you mean "speak"?
  17. }

(4)@deprecate (別名@deprecated)

deprecatedeprecated裝飾器在控制臺顯示一條警告,表示該方法將廢除。

  1. import { deprecate } from 'core-decorators';
  2. class Person {
  3. @deprecate
  4. facepalm() {}
  5. @deprecate('We stopped facepalming')
  6. facepalmHard() {}
  7. @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  8. facepalmHarder() {}
  9. }
  10. let person = new Person();
  11. person.facepalm();
  12. // DEPRECATION Person#facepalm: This function will be removed in future versions.
  13. person.facepalmHard();
  14. // DEPRECATION Person#facepalmHard: We stopped facepalming
  15. person.facepalmHarder();
  16. // DEPRECATION Person#facepalmHarder: We stopped facepalming
  17. //
  18. // See http://knowyourmeme.com/memes/facepalm for more details.
  19. //

(5)@suppressWarnings

suppressWarnings裝飾器抑制 deprecated裝飾器導(dǎo)致的 console.warn()調(diào)用。但是,異步代碼發(fā)出的調(diào)用除外。

  1. import { suppressWarnings } from 'core-decorators';
  2. class Person {
  3. @deprecated
  4. facepalm() {}
  5. @suppressWarnings
  6. facepalmWithoutWarning() {
  7. this.facepalm();
  8. }
  9. }
  10. let person = new Person();
  11. person.facepalmWithoutWarning();
  12. // no warning is logged

5. 使用裝飾器實現(xiàn)自動發(fā)布事件

我們可以使用裝飾器,使得對象的方法被調(diào)用時,自動發(fā)出一個事件。

  1. const postal = require("postal/lib/postal.lodash");
  2. export default function publish(topic, channel) {
  3. const channelName = channel || '/';
  4. const msgChannel = postal.channel(channelName);
  5. msgChannel.subscribe(topic, v => {
  6. console.log('頻道: ', channelName);
  7. console.log('事件: ', topic);
  8. console.log('數(shù)據(jù): ', v);
  9. });
  10. return function(target, name, descriptor) {
  11. const fn = descriptor.value;
  12. descriptor.value = function() {
  13. let value = fn.apply(this, arguments);
  14. msgChannel.publish(topic, value);
  15. };
  16. };
  17. }

上面代碼定義了一個名為publish的裝飾器,它通過改寫` descriptor.value`` ,使得原方法被調(diào)用時,會自動發(fā)出一個事件。它使用的事件“發(fā)布/訂閱”庫是Postal.js

它的用法如下。

  1. // index.js
  2. import publish from './publish';
  3. class FooComponent {
  4. @publish('foo.some.message', 'component')
  5. someMethod() {
  6. return { my: 'data' };
  7. }
  8. @publish('foo.some.other')
  9. anotherMethod() {
  10. // ...
  11. }
  12. }
  13. let foo = new FooComponent();
  14. foo.someMethod();
  15. foo.anotherMethod();

以后,只要調(diào)用 someMethod 或者 anotherMethod ,就會自動發(fā)出一個事件。

  1. $ bash-node index.js
  2. 頻道: component
  3. 事件: foo.some.message
  4. 數(shù)據(jù): { my: 'data' }
  5. 頻道: /
  6. 事件: foo.some.other
  7. 數(shù)據(jù): undefined

6. Mixin

在裝飾器的基礎(chǔ)上,可以實現(xiàn) Mixin 模式。所謂 Mixin模式,就是對象繼承的一種替代方案,中文譯為“混入”(mix in),意為在一個對象之中混入另外一個對象的方法。

請看下面的例子。

  1. const Foo = {
  2. foo() { console.log('foo') }
  3. };
  4. class MyClass {}
  5. Object.assign(MyClass.prototype, Foo);
  6. let obj = new MyClass();
  7. obj.foo() // 'foo'

上面代碼之中,對象 Foo 有一個 foo 方法,通過 Object.assign 方法,可以將 foo 方法“混入” MyClass 類,導(dǎo)致 MyClass 的實例 obj 對象都具有 foo 方法。這就是“混入”模式的一個簡單實現(xiàn)。

下面,我們部署一個通用腳本 mixins.js ,將 Mixin 寫成一個裝飾器。

  1. export function mixins(...list) {
  2. return function (target) {
  3. Object.assign(target.prototype, ...list);
  4. };
  5. }

然后,就可以使用上面這個裝飾器,為類“混入”各種方法。

  1. import { mixins } from './mixins';
  2. const Foo = {
  3. foo() { console.log('foo') }
  4. };
  5. @mixins(Foo)
  6. class MyClass {}
  7. let obj = new MyClass();
  8. obj.foo() // "foo"

通過 mixins 這個裝飾器,實現(xiàn)了在 MyClass 類上面“混入” Foo 對象的 foo 方法。

不過,上面的方法會改寫 MyClass 類的 prototype 對象,如果不喜歡這一點,也可以通過類的繼承實現(xiàn) Mixin。

  1. class MyClass extends MyBaseClass {
  2. /* ... */
  3. }

上面代碼中, MyClass 繼承了 MyBaseClass 。如果我們想在 MyClass 里面“混入”一個 foo 方法,一個辦法是在 MyClass 和 MyBaseClass 之間插入一個混入類,這個類具有 foo 方法,并且繼承了 MyBaseClass 的所有方法,然后 MyClass 再繼承這個類。

  1. let MyMixin = (superclass) => class extends superclass {
  2. foo() {
  3. console.log('foo from MyMixin');
  4. }
  5. };

上面代碼中, MyMixin 是一個混入類生成器,接受 superclass 作為參數(shù),然后返回一個繼承 superclass 的子類,該子類包含一個 foo 方法。

接著,目標(biāo)類再去繼承這個混入類,就達(dá)到了“混入” foo 方法的目的。

  1. class MyClass extends MyMixin(MyBaseClass) {
  2. /* ... */
  3. }
  4. let c = new MyClass();
  5. c.foo(); // "foo from MyMixin"

如果需要“混入”多個方法,就生成多個混入類。

  1. class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  2. /* ... */
  3. }

這種寫法的一個好處,是可以調(diào)用 super ,因此可以避免在“混入”過程中覆蓋父類的同名方法。

  1. let Mixin1 = (superclass) => class extends superclass {
  2. foo() {
  3. console.log('foo from Mixin1');
  4. if (super.foo) super.foo();
  5. }
  6. };
  7. let Mixin2 = (superclass) => class extends superclass {
  8. foo() {
  9. console.log('foo from Mixin2');
  10. if (super.foo) super.foo();
  11. }
  12. };
  13. class S {
  14. foo() {
  15. console.log('foo from S');
  16. }
  17. }
  18. class C extends Mixin1(Mixin2(S)) {
  19. foo() {
  20. console.log('foo from C');
  21. super.foo();
  22. }
  23. }

上面代碼中,每一次混入發(fā)生時,都調(diào)用了父類的 super.foo 方法,導(dǎo)致父類的同名方法沒有被覆蓋,行為被保留了下來。

  1. new C().foo()
  2. // foo from C
  3. // foo from Mixin1
  4. // foo from Mixin2
  5. // foo from S

7. Trait

Trait也是一種裝飾器,效果與Mixin 類似,但是提供更多功能,比如防止同名方法的沖突、排除混入某些方法、為混入的方法起別名等等。

下面采用traits-decorator這個第三方模塊作為例子。這個模塊提供的 traits 裝飾器,不僅可以接受對象,還可以接受 ES6 類作為參數(shù)。

  1. import { traits } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') }
  7. };
  8. @traits(TFoo, TBar)
  9. class MyClass { }
  10. let obj = new MyClass();
  11. obj.foo() // foo
  12. obj.bar() // bar

上面代碼中,通過 traits 裝飾器,在 MyClass 類上面“混入”了 TFoo 類的 foo 方法和 TBar 對象的 bar 方法。

Trait 不允許“混入”同名方法。

  1. import { traits } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar)
  10. class MyClass { }
  11. // 報錯
  12. // throw new Error('Method named: ' + methodName + ' is defined twice.');
  13. // ^
  14. // Error: Method named: foo is defined twice.

上面代碼中, TFoo 和 TBar 都有 foo 方法,結(jié)果 traits 裝飾器報錯。

一種解決方法是排除 TBar 的 foo 方法。

  1. import { traits, excludes } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar::excludes('foo'))
  10. class MyClass { }
  11. let obj = new MyClass();
  12. obj.foo() // foo
  13. obj.bar() // bar

上面代碼使用綁定運算符(::)在 TBar 上排除 foo 方法,混入時就不會報錯了。

另一種方法是為 TBar 的 foo 方法起一個別名。

  1. import { traits, alias } from 'traits-decorator';
  2. class TFoo {
  3. foo() { console.log('foo') }
  4. }
  5. const TBar = {
  6. bar() { console.log('bar') },
  7. foo() { console.log('foo') }
  8. };
  9. @traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
  10. class MyClass { }
  11. let obj = new MyClass();
  12. obj.foo() // foo
  13. obj.aliasFoo() // foo
  14. obj.bar() // bar

上面代碼為 TBar 的 foo 方法起了別名 aliasFoo ,于是 MyClass 也可以混入 TBar 的 foo 方法了。

alias 和 excludes 方法,可以結(jié)合起來使用。

  1. @traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
  2. class MyClass {}

上面代碼排除了 TExample 的 foo 方法和 bar 方法,為 baz 方法起了別名 exampleBaz 。

as 方法則為上面的代碼提供了另一種寫法。

  1. @traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
  2. class MyClass {}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號