當咱們拍了一張照片準備發朋友圈時,許多小夥伴會選擇給照片加上濾鏡。同一張照片、不一樣的濾鏡組合起來就會有不一樣的體驗。這裏實際上就應用了裝飾者模式:是經過濾鏡裝飾了照片。在不改變對象(照片)的狀況下動態的爲其添加功能(濾鏡)。javascript
須要注意的是:因爲 JavaScript 語言動態的特性,咱們很容易就能改變某個對象(JavaScript 中函數是一等公民)。可是咱們要儘可能避免直接改寫某個函數,這會致使代碼的可維護性、可擴展性變差,甚至會污染其餘業務。html
想必你們對"餐前洗手、飯後漱口"都不陌生。這句標語其實就是 AOP 在生活中的例子:吃飯這個動做至關於切點,咱們能夠在這個切點前、後插入其它如洗手等動做。vue
AOP(Aspect-Oriented Programming):面向切面編程,是對 OOP 的補充。利用AOP能夠對業務邏輯的各個部分進行隔離,也能夠隔離業務無關的功能好比日誌上報、異常處理等,從而使得業務邏輯各部分之間的耦合度下降,提升業務無關的功能的複用性,也就提升了開發的效率。java
在 JavaScript 中,咱們能夠經過裝飾者模式來實現 AOP,可是二者並非一個維度的概念。 AOP 是一種編程範式,而裝飾者是一種設計模式。es6
瞭解了裝飾者模式和 AOP 的概念以後,咱們寫一段可以兼容 ES3 的代碼來實現裝飾者模式:typescript
// 原函數
var takePhoto =function(){
console.log('拍照片');
}
// 定義 aop 函數
var after=function( fn, afterfn ){
return function(){
let res = fn.apply( this, arguments );
afterfn.apply( this, arguments );
return res;
}
}
// 裝飾函數
var addFilter=function(){
console.log('加濾鏡');
}
// 用裝飾函數裝飾原函數
takePhoto=after(takePhoto,addFilter);
takePhoto();
複製代碼
這樣咱們就實現了抽離拍照與濾鏡邏輯,若是之後須要自動上傳功能,也能夠經過aop
函數after
來添加。編程
在 ES5 中引入了Object.defineProperty
,咱們能夠更方便的給對象添加屬性:設計模式
let takePhoto = function () {
console.log('拍照片');
}
// 給 takePhoto 添加屬性 after
Object.defineProperty(takePhoto, 'after', {
writable: true,
value: function () {
console.log('加濾鏡');
},
});
// 給 takePhoto 添加屬性 before
Object.defineProperty(takePhoto, 'before', {
writable: true,
value: function () {
console.log('打開相機');
},
});
// 包裝方法
let aop = function (fn) {
return function () {
fn.before()
fn()
fn.after()
}
}
takePhoto = aop(takePhoto)
takePhoto()
複製代碼
咱們知道,在 JavaScript 中,函數也好,類也好都有着本身的原型,經過原型鏈咱們也可以很方便的動態擴展,如下是基於原型鏈的寫法:app
class Test {
takePhoto() {
console.log('拍照');
}
}
// after AOP
function after(target, action, fn) {
let old = target.prototype[action];
if (old) {
target.prototype[action] = function () {
let self = this;
fn.bind(self);
fn(handle);
}
}
}
// 用 AOP 函數修飾原函數
after(Test, 'takePhoto', () => {
console.log('添加濾鏡');
});
let t = new Test();
t.takePhoto();
複製代碼
在 ES7 中引入了@decorator 修飾器的提案,參考阮一峯的文章。修飾器是一個函數,用來修改類的行爲。目前Babel轉碼器已經支持。注意修飾器只能裝飾類或者類屬性、方法。三者的具體區別請參考 MDN Object.defineProperty ;而 TypeScript 的實現又有所不一樣:TypeScript Decorator。異步
接下來咱們經過修飾器來實現對方法的裝飾:
function after(target, key, desc) {
const { value } = desc;
desc.value = function (...args) {
let res = value.apply(this, args);
console.log('加濾鏡')
return res;
}
return desc;
}
class Test{
@after
takePhoto(){
console.log('拍照')
}
}
let t = new Test()
t.takePhoto()
複製代碼
能夠看到,使用修飾器的代碼很是簡潔明瞭。
裝飾者模式能夠應用在不少場景,典型的場景是記錄某異步請求請求耗時的性能數據並上報:
function report(target, key, desc) {
const { value } = desc;
desc.value = async function (...args) {
let start = Date.now();
let res = await value.apply(this, args);
let millis = Date.now()-start;
// 上報代碼
return res;
}
return desc;
}
class Test{
@report
getData(url){
// fetch 代碼
}
}
let t = new Test()
t.getData()
複製代碼
這樣使用@report
修飾後的代碼就會上報請求所消耗的時間。擴展或者修改report
函數不會影響業務代碼,反之亦然。
咱們能夠對原有代碼進行簡單的異常處理,而無需侵入式的修改:
function handleError(target, key, desc) {
const { value } = desc;
desc.value = async function (...args) {
let res;
try{
res = await value.apply(this, args);
}catch(err){
// 異常處理
logger.error(err)
}
return res;
}
return desc;
}
class Test{
@handleError
getData(url){
// fetch 代碼
}
}
let t = new Test()
t.getData()
複製代碼
經過以上兩個示例咱們能夠看到,修飾器的定義很簡單,功能卻很是強大。
咱們一步一步經過高階函數、原型鏈、Object.defineProperty
和@Decorator
分別實現了裝飾者模式。接下來在回顧一下:
有些朋友可能會以爲裝飾者模式和 vue 的 mixin 機制很像,其實他們都是「開放-封閉原則」和「單一職責原則」的體現。
附上代碼 jsbin 地址: