細說ES7 JavaScript Decorators


開篇概述

在上篇的ES7之Decorators實現AOP示例中,我們預先體驗了ES7的Decorators,雖然它只是一個簡單的日志AOP攔截Demo。但它也足以讓我們體會到ES7 Decorators的強大魅力所在。所以為什么博主會為它而專門寫作此文。在Angular2中的TypeScript Annotate就是標注裝潢器的另一類實現。同樣如果你也是一個React的愛好者,你應該已經發現了redux2中也開始利用ES7的Decorators進行了大量重構。

嘗試過Python的同學們,我相信你做難忘的應該是裝潢器。由Yehuda Katz提出的decorator模式,就是借鑒於Python的這一特性。作為讀者的你,可以從上一篇博文ES7之Decorators實現AOP示例中看到它們之間的聯系。

Decorators

背后原理

ES7的Decorators讓我們能夠在設計時對類、屬性等進行標注和修改成為了可能。Decorators利用了ES5的

Object.defineProperty(target, name, descriptor);

來實現這一特性。如果你還不了解Object.defineProperty,請參見MDN文檔。首先我們來考慮一個普通的ES6類:

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

執行這一段class,給Person.prototype注冊一個name屬性,粗略的和如下代碼相似:

Object.defineProperty(Person.prototype, 'name', {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
});

如果利用裝潢器來標注一個屬性呢?

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

在這種裝潢下的屬性,則會在利用Object.defineProperty為Person.prototype注冊name屬性之前,執行這段裝潢器:

let descriptor = {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,enumerable、
  writable: true
};

descriptor = readonly(Person.prototype, 'name', descriptor) || descriptor;
Object.defineProperty(Person.prototype, 'name', descriptor);

從上面的偽代碼中,我們能看出,裝潢器只是在Object.defineProperty為Person.prototype注冊屬性之前,執行一個裝飾函數,其屬於一類對Object.defineProperty的攔截。所以它和Object.defineProperty具有一致的方法簽名,它們的3個參數分別為:

  1. obj:作用的目標對象;
  2. prop:作用的屬性名;
  3. descriptor: 針對該屬性的描述符。

這里最重要的是descriptor這個參數,它是一個數據或訪問器的屬性描述對象。在對數據和訪問器屬性描述時,它們都具有configurable、enumerable屬性可用。而在數據描述時,value、writable屬性則是數據所特有的。get、set屬性則是訪問器屬性描述所特有的。屬性描述器中的屬性決定了對象prop屬性的一些特性。比如 enumerable,它決定了目標對象是否可被枚舉,能夠在for…in循環中遍歷到,或者出現在Object.keys法的返回值中;writable則決定了目標對象的屬性是否可以被更改。完整的屬性描述,請參見MDN文檔

對於descriptor中的屬性,它們可以被我們在Decorators中使用,或者修改的,以達到我們標注或者攔截的目的。這也是裝潢器攔截的主體信息。

作用於訪問器

裝潢器也可以作用與屬性的getter/setter訪問器之上,如下將屬性標注為不可枚舉的代碼:

class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

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

下面是一個更復雜的對訪問器的備用錄模式運用:

class Person {
  @memoize
  get name() { return `${this.first} ${this.last}` }
  set name(val) {
    let [first, last] = val.split(' ');
    this.first = first;
    this.last = last;
  }
}

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;
  }
}

function memoizationFor(obj) {
  let table = memoized.get(obj);
  if (!table) { table = Object.create(null); memoized.set(obj, table); }
  return table;
}   

作用域類上

同樣Decorators也可以為class裝潢,如下對類是否annotated的標注:

// A simple decorator
@annotation
class MyClass { }

function annotation(target) {
   // Add a property on target
   target.annotated = true;
}

也可以是一個工廠方法

對於裝潢器來說,它同樣也可以是一個工廠方法,接受配置參數信息,並返回一個應用於目標函數的裝潢函數。如下例子,對類可測試性的標記:

@isTestable(true)
class MyClass { }

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

同樣工廠方法,也可以被應用於屬性之上,如下對可枚舉屬性的配置:

class C {
  @enumerable(false)
  method() { }
}

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

同樣在上篇ES7之Decorators實現AOP示例中對於日志攔截的日志類型配置信息,也是利用工廠方法來實現的。它是一個更復雜的工廠方式的Decorators實現。

后續

如上一篇博問所說:雖然它是ES7的特性,但在Babel大勢流行的今天,我們可以利用Babel來使用它。我們可以利用Babel命令行工具,或者grunt、gulp、webpack的babel插件來使用Decorators。

關於ES7 Decorators的更有意思的玩法,你可以參見牛人實現的常用的Decorators:core-decorators。以及raganwald如何用Decorators來實現Mixin


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM