1. 程式人生 > >ES6之修飾器Decorator

ES6之修飾器Decorator

  1. 類的修飾
    許多面向物件的語言都有修飾器(Decorator)函式,用來修改類的行為。
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true
上面程式碼中,@testable就是一個修飾器。它修改了MyTestableClass這個類的行為,為它加上了靜態屬性isTestable。testable函式的引數target是MyTestableClass類本身。

修飾器是一個對類進行處理的函式。修飾器函式的第一個引數,就是所要修飾的目標類。如果覺得一個引數不夠用,可以在修飾器外面再封裝一層函式。

//testable函式的引數target,就是會被修飾的類。
function testable(target) {
  // ...
}

//多個引數
function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

修飾器testable可以接受引數,這就等於可以修改修飾器的行為。

注意,修飾器對類的行為的改變,是程式碼編譯時發生的,而不是在執行時。這意味著,修飾器能在編譯階段執行程式碼。也就是說,修飾器本質就是編譯時執行的函式。
可以通過目標類的prototype物件操作新增例項屬性。(上面的例子是靜態屬性)

function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true
上面程式碼中,修飾器函式testable是在目標類的prototype物件上新增屬性,因此就可以在例項上呼叫。
React 與 Redux 庫結合使用時,常常需要寫成下面這樣。

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
有了裝飾器,就可以改寫上面的程式碼。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
  1. 方法的修飾
    修飾器函式readonly一共可以接受三個引數。
    第一個引數是類的原型物件,上例是Person.prototype,修飾器的本意是要“修飾”類的例項,但是這個時候例項還沒生成,所以只能去修飾原型(這不同於類的修飾,那種情況時target引數指的是類本身);
    第二個引數是所要修飾的屬性名;
    第三個引數是該屬性的描述物件。
function readonly(target, name, descriptor){
  // descriptor物件原來的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 類似於
Object.defineProperty(Person.prototype, 'name', descriptor);
//修飾器(readonly)會修改屬性的描述物件(descriptor),然後被修改的描述物件再用來定義屬性。

@log修飾器,可以起到輸出日誌的作用
@log修飾器的作用就是在執行原始的操作之前,執行一次console.log,從而達到輸出日誌的目的。

修飾器有註釋的作用。
如果同一個方法有多個修飾器,會像剝洋蔥一樣,先從外到內進入,然後由內向外執行。
修飾器還能用來型別檢查。所以,對於類來說,這項功能相當有用。從長期來看,它將是 JavaScript 程式碼靜態分析的重要工具。

  1. 修飾器不能用去函式
    修飾器只能用於類和類的方法,不能用於函式,因為存在函式提升。由於存在函式提升,使得修飾器不能用於函式。類是不會提升的,所以就沒有這方面的問題。
另一方面,如果一定要修飾函式,可以採用高階函式的形式直接執行。

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);
  1. core-decorators.js
    core-decorators.js是一個第三方模組,提供了幾個常見的修飾器,通過它可以更好地理解修飾器。

(1)@autobind
autobind修飾器使得方法中的this物件,繫結原始物件。

(2)@readonly
readonly修飾器使得屬性或方法不可寫。

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

(4)@deprecate (別名@deprecated)
deprecate或deprecated修飾器在控制檯顯示一條警告,表示該方法將廢除。

(5)@suppressWarnings
suppressWarnings修飾器抑制deprecated修飾器導致的console.warn()呼叫。但是,非同步程式碼發出的呼叫除外。

  1. 修飾器實現自動釋出事件
    我們可以使用修飾器,使得物件的方法被呼叫時,自動發出一個事件。
    只要呼叫someMethod或者anotherMethod,就會自動發出一個事件。
// index.js
import publish from './publish';

class FooComponent {
  @publish('foo.some.message', 'component')
  someMethod() {
    return { my: 'data' };
  }
  @publish('foo.some.other')
  anotherMethod() {
    // ...
  }
}

let foo = new FooComponent();

foo.someMethod();
foo.anotherMethod();
以後,只要呼叫someMethod或者anotherMethod,就會自動發出一個事件。

$ bash-node index.js
頻道:  component
事件:  foo.some.message
資料:  { my: 'data' }

頻道:  /
事件:  foo.some.other
資料:  undefined
  1. Mixin
    在修飾器的基礎上,可以實現Mixin模式。所謂Mixin模式,就是物件繼承的一種替代方案,中文譯為“混入”(mix in),意為在一個物件之中混入另外一個物件的方法。

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

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

這種寫法的一個好處,是可以呼叫super,因此可以避免在“混入”過程中覆蓋父類的同名方法。

  1. Trait
    Trait 也是一種修飾器,效果與 Mixin 類似,但是提供更多功能,比如防止同名方法的衝突、排除混入某些方法、為混入的方法起別名等等。
    Trait 不允許“混入”同名方法。
    alias和excludes方法,可以結合起來使用。
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}
上面程式碼排除了TExample的foo方法和bar方法,為baz方法起了別名exampleBaz。

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

@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}
  1. Babel轉碼器的支援
    Babel 轉碼器已經支援 Decorator。
    首先,安裝babel-core和babel-plugin-transform-decorators。由於後者包括在babel-preset-stage-0之中,所以改為安裝babel-preset-stage-0亦可。
    然後,設定配置檔案.babelrc。
//首先,安裝babel-core和babel-plugin-transform-decorators
$ npm install babel-core babel-plugin-transform-decorators

//然後,設定配置檔案.babelrc
{
  "plugins": ["transform-decorators"]
}
//這時,Babel 就可以對 Decorator 轉碼了。

//指令碼中開啟的命令如下。
babel.transform("code", {plugins: ["transform-decorators"]})
  1. 詳細資訊請參考 http://es6.ruanyifeng.com/#docs/decorator