開篇概述
在上篇的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個參數分別為:
- obj:作用的目標對象;
- prop:作用的屬性名;
- 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。