
現(xiàn)在什么 AOP 編程在前端領(lǐng)域越來(lái)越被大家追捧,所以我也來(lái)探究一下如何在javascript中進(jìn)行AOP編程。 裝飾器無(wú)疑是對(duì)AOP最有力的設(shè)計(jì),在es5 時(shí)代,可以通過(guò)
Object.defineProperty來(lái)對(duì)對(duì)象屬性/方法 進(jìn)行訪問(wèn)修飾,但用起來(lái)需要寫(xiě)一堆東西。現(xiàn)在decorator已經(jīng)在ES7的提案中了,借助Babel等轉(zhuǎn)碼工具,我們現(xiàn)在也能在javascript中使用裝飾器語(yǔ)法了!
什么是Decorator
decorator 也叫裝飾器(裝潢器)。它可以在不侵入到原有代碼內(nèi)部的情況下而通過(guò)標(biāo)注的方式修改類(lèi)代碼行為,裝飾器對(duì)代碼行為的改變是在編譯階段完成的,而不是在執(zhí)行階段。雖然Decorator還處在ES7草案階段,但是我們可以通過(guò)Babel來(lái)轉(zhuǎn)換es7代碼,所以大家還是可以愉快的使用decorator。
在ES7提案中,Decorator的描述如下:
- an expression
- that evaluates to a function
- that takes the target, name, and decorator descriptor as arguments
- and optionally returns a decorator descriptor to install on the target object.
出自 https://github.com/wycats/javascript-decorators
在代碼層面,Decorator其實(shí)就是一個(gè)函數(shù)。
function readonly(target, name, desc) {
desc.writable = false;
return desc;
}
let o = {
@readonly // 標(biāo)識(shí)為只讀屬性
name: 'liuyan'
}
// 賦值失敗并報(bào)錯(cuò)
o.name = 'liuzheng'; // Cannot assign to read only property 'name' of object '#<Object>'
上面的代碼實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的裝飾器用來(lái)使對(duì)象屬性只讀。函數(shù)readonly 規(guī)定了裝飾器描述符的行為。不難看出,這和ES5中的 Object.defineProperty 方法很類(lèi)似,使用es5代碼一樣能夠?qū)崿F(xiàn)相同的功能,其實(shí)使用Babel轉(zhuǎn)碼最終也就是轉(zhuǎn)換成了Object.defineProperty 的實(shí)現(xiàn)形式,只是使用 @readonly 這種語(yǔ)法更能直觀的描述出來(lái), 對(duì)比Java中的注解、 Python中的裝飾器其實(shí)都使用類(lèi)似的語(yǔ)法。
Decorator用法
給屬性添加Decorator
和前面的例子一樣,有時(shí)候需要在JS中實(shí)現(xiàn)類(lèi)靜態(tài)成員,這個(gè)時(shí)候就可以使用Decorator來(lái)修飾了,代碼如下:
// 示例
class Person {
@readonly
static MIN_AGE = 0;
}
這樣,當(dāng)不小心重新為 Person.MIN_AGE 賦值的時(shí)候,就會(huì)拋出錯(cuò)誤。
給方法添加Decorator
也可以對(duì)方法進(jìn)行裝飾。比如現(xiàn)在需要實(shí)現(xiàn)一個(gè)功能: 設(shè)計(jì)一個(gè)裝飾器,它能夠統(tǒng)計(jì)出一個(gè)異步方法(這里只用Promise)的耗時(shí)。還是以Person類(lèi)為例,給Person增加一個(gè)request方法,統(tǒng)計(jì)request執(zhí)行耗時(shí),代碼實(shí)現(xiàn)非常簡(jiǎn)單:
class Person {
static MIN_AGE = 0;
constructor(name) {
this.name = name
}
@duration
request() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({status: 0})
}, 3000);
})
}
}
// 裝飾器
function duration(target, key, desc) {
const { value } = desc;
let _time = Date.now();
desc.value = function(...args) {
let res = value.apply(this, args);
if (res && typeof res.then === 'function') {
res.then(() => {
console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
}, () => {
console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
})
} else {
console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
}
return res;
}
// 需要把描述對(duì)象返回
return desc;
}
// 開(kāi)始
var p = new Person('liuyan');
p.request(); // 輸出: request() ==> 耗時(shí):3002ms
作用于class
也可以在為class 應(yīng)用裝飾器,現(xiàn)在我要通過(guò)裝飾器給Person類(lèi)增加一個(gè)靜態(tài)屬性IS_PERSON; (當(dāng)然,這沒(méi)什么卵用...)
// 增加靜態(tài)屬性IS_PERSON
@isPerson
class Person {
...
}
function isPerson(target) {
target.IS_PERSON = true;
}
console.log(Person.IS_PERSON); // true
也可以作用于class的實(shí)例屬性
class Person {
...
}
function sayHi(target) {
const {sayHi} = target.prototype;
target.prototype.sayHi = function(...args) {
if (typeof sayHi === 'function') {
var res = sayHi.apply(this, args);
}
console.log(`Hi, I\'m ${this.name}`);
return res;
};
}
var p = new Person('liuyan');
p.sayHi(); // Hi, I'm liuyan
decorator作用于類(lèi)最常見(jiàn)的用法就是mixins了,mixin 也就是允許我們?yōu)榻M件(類(lèi)) 附加額外的功能,用過(guò)react的童鞋應(yīng)該對(duì)mixin不陌生,不過(guò)使用mixin擴(kuò)展新功能這種用法已經(jīng)不被推薦了。
decorator已經(jīng)在各知名框架中開(kāi)始大面積使用,比如Angular2(ng2), 雖然ng2使用TypeScript 來(lái)構(gòu)建的,但是裝飾器這種語(yǔ)法實(shí)現(xiàn)也是大同小異的。下圖是從angular js官網(wǎng)截取的示例代碼:

實(shí)際使用場(chǎng)景(Logger)
一個(gè)東西被吹得再好,如果沒(méi)有使用場(chǎng)景那也是白搭。
在實(shí)際業(yè)務(wù)中,很多時(shí)候把裝飾器用在日志工具上面,因?yàn)槿罩具@種東西和業(yè)務(wù)幾乎是完全分離的,試想一下,如果業(yè)務(wù)代碼里面參雜了各種各樣的日志信息...., 對(duì)于閱讀代碼邏輯以及維護(hù)來(lái)說(shuō)都是災(zāi)難性的,這個(gè)時(shí)候我們的decorator就能派上用場(chǎng)了。
假設(shè)需要實(shí)現(xiàn)一個(gè)對(duì)定時(shí)任務(wù)的監(jiān)控logger, 需要監(jiān)控何時(shí)開(kāi)始、結(jié)束,以及任務(wù)運(yùn)行耗時(shí)的信息。代碼如下
class ScheduleJob {
constructor(name) {
this.name = name;
}
@log('info', '開(kāi)始')
start() {
setTimeout(() => {
this.stop();
}, 2000);
}
@log('info', '結(jié)束')
stop() {}
}
var job = new ScheduleJob('liuyan');
job.start();
//輸出:
//Thu Jan 05 2017 18:34:09 GMT+0800 (CST) - info - 開(kāi)始
//Thu Jan 05 2017 18:34:09 GMT+0800 (CST) - info - 開(kāi)始....time: 1483612449481
//Thu Jan 05 2017 18:34:12 GMT+0800 (CST) - info - 結(jié)束
//Thu Jan 05 2017 18:34:12 GMT+0800 (CST) - info - 結(jié)束....time: 1483612452258,耗時(shí):2777ms
function log(t = 'info', msg = '') {
return function(target, name, desc) {
const {value} = desc;
desc.value = function(...args) {
console.log(`${new Date()} - ${t} - ${msg} `)
let res = value.apply(this, args);
if(name === 'start') {
this[`startTime`] = Date.now();
console.log(`${new Date()} - ${t} - 開(kāi)始....time: ${this[`startTime`]}`)
}
if( name === 'stop' ) {
this[`endTime`] = Date.now();
console.log(`${new Date()} - ${t} - 結(jié)束....time: ${this[`endTime`]},耗時(shí):${this[`endTime`] - this[`startTime`]}ms`)
}
}
}
}
上面的是一個(gè)很簡(jiǎn)單的需求,我們沒(méi)有修改原有類(lèi)的任何代碼就實(shí)現(xiàn)了日志監(jiān)控。其實(shí)這種實(shí)現(xiàn)在編程器思想里面叫做 AOP,中文名也叫面向切面編程,java里面用得非常之多。
網(wǎng)上有牛人寫(xiě)了一些常用的decorators core-decorators,源碼比較簡(jiǎn)單,可以學(xué)習(xí)學(xué)習(xí)。
參考資料
decorator描述: https://github.com/wycats/javascript-decorators
core-decorators.js https://github.com/jayphelps/core-decorators.js
decorators 文檔 http://tc39.github.io/proposal-decorators/