1. 程式人生 > 實用技巧 >ECMAScript 6-修飾器Decorator

ECMAScript 6-修飾器Decorator

修飾器(Decorator)是一個函式,用來修改類的行為。修飾器對類的行為的改變,是程式碼編譯時發生的,而不是在執行時。這意味著,修飾器能在編譯階段執行程式碼

類的修飾

//修飾器函式1:為類加上靜態屬性isTestable
//修飾器函式的第一個引數,就是所要修飾的目標類
function testable(target) {
  target.isTestable = true;
}

//修飾類的行為。寫在類上方,表示修飾器的目標是這整個類
@testable
class MyTestableClass {}

console.log(MyTestableClass.isTestable) // true
//修飾器函式2:新增例項屬性
function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

修飾器的實際行為

@decorator
class A {}

// 等同於
class A {}
A = decorator(A) || A;

類的方法的修飾

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

修飾器的引數

修飾器函式一共可以接受三個引數:

  • 所要修飾的目標物件
  • 所要修飾的屬性名
  • 該屬性的描述物件
function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false; //修改了描述物件的屬性enumerable,使得不可遍歷
  return descriptor;
}

class Person {
  @nonenumerable //修飾該屬性
  get kidCount() { return this.children.length; }
}

@log修飾器例子:輸出日誌

function log(target, name, descriptor) {
  var oldValue = descriptor.value;
  descriptor.value = function() {
    console.log(`Calling "${name}" with`, arguments);
    return oldValue.apply(null, arguments);
  };
  return descriptor;
}

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}


const math = new Math();
// passed parameters should get logged now
math.add(2, 4);

修飾器有註釋作用

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}
//可看出Person類是可測試的,而name方法是隻讀和不可列舉的

core-decorators.js

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

  • @autobind:使得方法中的this物件,繫結原始物件

    import { autobind } from 'core-decorators';
    
    class Person {
      @autobind
      getPerson() {
        return this;
      }
    }
    
    let person = new Person(); 
    let getPerson = person.getPerson;
    
    getPerson() === person; 	// true
    
  • @readonly:使得屬性或方法不可寫

    import { readonly } from 'core-decorators';
    
    class Meal {
      @readonly
      entree = 'steak';
    }
    
    var dinner = new Meal();
    dinner.entree = 'salmon';
    // Cannot assign to read only property 'entree' of [object Object]
    
  • @override:檢查子類的方法是否正確覆蓋了父類的同名方法,如果不正確會報錯

    import { override } from 'core-decorators';
    
    class Parent {
      speak(first, second) {}
    }
    
    class Child extends Parent {
      @override
      speak() {}// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
    }
    
  • @deprecate (別名@deprecated):在控制檯顯示一條警告,表示該方法將廢除

    import { deprecate } from 'core-decorators';
    
    class Person {
      @deprecate
      facepalm() {}
    
      @deprecate('We stopped facepalming')
      facepalmHard() {}
    
      @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
      facepalmHarder() {}
    }
    
    let person = new Person();
    
    person.facepalm();
    // DEPRECATION Person#facepalm: This function will be removed in future versions.
    
    person.facepalmHard();
    // DEPRECATION Person#facepalmHard: We stopped facepalming
    
    person.facepalmHarder();
    // DEPRECATION Person#facepalmHarder: We stopped facepalming
    //
    //     See http://knowyourmeme.com/memes/facepalm for more details.
    //
    

Mixin

在修飾器的基礎上,可以實現Mixin模式。所謂Mixin模式,就是物件繼承的一種替代方案,中文譯為“混入”(mixin),意為在一個物件之中混入另外一個物件的方法

以往寫法:

//一個物件,裡面有個foo方法
const Foo = {
  foo() { console.log('foo') }
};

//一個類
class MyClass {}

//通過Object.assign在類的原型物件上新增Foo物件上的方法新增進去,
Object.assign(MyClass.prototype, Foo);

let obj = new MyClass();
obj.foo() // 'foo'

現在:部署一個通用指令碼mixins.js,將mixin寫成一個修飾器

export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}
//原理:傳入引數(這裡是物件型別),然後將該引數中的方法新增在類的原型物件中

使用

import { mixins } from './mixins';

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo) //傳入Foo物件,把Foo物件中foo方法的新增在MyClass的原型物件中
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"

Trait修飾器

效果與Mixin類似,但是提供更多功能,比如防止同名方法的衝突、排除混入某些方法、為混入的方法起別名等等

下面採用traits-decorator這個第三方模組作為例子。這個模組提供的traits修飾器,不僅可以接受物件,還可以接受ES6類作為引數

import { traits } from 'traits-decorator';

//TFoo類中有foo方法
class TFoo {
  foo() { console.log('foo') }
}

//TBar物件中有bar方法
const TBar = {
  bar() { console.log('bar') }
};

//把TFoo類中有foo方法和TBar物件中有bar方法加進MyClass類的原型物件中
@traits(TFoo, TBar)
class MyClass { }

let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar

另外,Trait不允許“混入”同名方法

Babel轉碼器的支援

目前,Babel轉碼器已經支援Decorator。

首先,安裝babel-corebabel-plugin-transform-decorators。由於後者包括在babel-preset-stage-0之中,所以改為安裝babel-preset-stage-0亦可

$ npm install babel-core babel-plugin-transform-decorators

然後,設定配置檔案.babelrc

{
  "plugins": ["transform-decorators"]
}

這時,Babel就可以對Decorator轉碼了

指令碼中開啟的命令如下

babel.transform("code", {plugins: ["transform-decorators"]})