裝飾器模式
裝飾模式和適配器模式都是 包裝模式 (Wrapper Pattern)骏令,它們都是通過封裝其他對象達(dá)到設(shè)計的目的的则果,但是它們的形態(tài)有很大區(qū)別岳掐。
適配器模式我們使用的場景比較多(繼承方案),比如連接不同數(shù)據(jù)庫的情況哮奇,你需要包裝現(xiàn)有的模塊接口膛腐,從而使之適配數(shù)據(jù)庫 —— 好比你手機使用轉(zhuǎn)接口來適配插座那樣;
-
裝飾模式不一樣屏镊,僅僅包裝現(xiàn)有的模塊依疼,使之 “更加華麗” 痰腮,并不會影響原有接口的功能 —— 好比你給手機添加一個外殼罷了而芥,并不影響手機原有的通話、充電等功能膀值;
更多區(qū)別參見:設(shè)計模式——裝飾模式(Decorator)
裝飾模式場景 —— 面向 AOP 編程
裝飾模式經(jīng)典的應(yīng)用是 AOP 編程棍丐,比如“日志系統(tǒng)”误辑,日志系統(tǒng)的作用是記錄系統(tǒng)的行為操作,它在不影響原有系統(tǒng)的功能的基礎(chǔ)上增加記錄環(huán)節(jié) —— 好比你佩戴了一個智能手環(huán)歌逢,并不影響你日常的作息起居巾钉,但你現(xiàn)在卻有了自己每天的行為記錄。
更加抽象的理解秘案,可以理解為給數(shù)據(jù)流做一層filter
砰苍,因此 AOP 的典型應(yīng)用包括 安全檢查、緩存阱高、調(diào)試赚导、持久化等等〕嗑可參考Spring aop 原理及各種應(yīng)用場景 吼旧。
我們知道,面向?qū)ο蟮奶攸c是繼承未舟、多態(tài)和封裝圈暗。而封裝就要求將功能分散到不同的對象中去,這在軟件設(shè)計中往往稱為職責(zé)分配裕膀。實際上也就是說员串,讓不同的類設(shè)計不同的方法。這樣代碼就分散到一個個的類中去了昼扛。這樣做的好處是降低了代碼的復(fù)雜程度昵济,使類可重用。
但是人們也發(fā)現(xiàn)野揪,在分散代碼的同時访忿,也增加了代碼的重復(fù)性。什么意思呢斯稳?比如說海铆,我們在兩個類中,可能都需要在每個方法中做日志挣惰。按面向?qū)ο蟮脑O(shè)計方法卧斟,我們就必須在兩個類的方法中都加入日志的內(nèi)容。也許他們是完全相同的憎茂,但就是因為面向?qū)ο蟮脑O(shè)計讓類與類之間無法聯(lián)系珍语,而不能將這些重復(fù)的代碼統(tǒng)一起來。
也許有人會說竖幔,那好辦啊板乙,我們可以將這段代碼寫在一個獨立的類獨立的方法里,然后再在這兩個類中調(diào)用。但是募逞,這樣一來蛋铆,這兩個類跟我們上面提到的獨立的類就有耦合了,它的改變會影響這兩個類放接。那么刺啦,有沒有什么辦法,能讓我們在需要的時候纠脾,隨意地加入代碼呢玛瘸?這種在運行時,動態(tài)地將代碼切入到類的指定方法苟蹈、指定位置上的編程思想就是面向切面的編程捧韵。
什么是裝(修)飾器
裝飾器Decorator是在es2016(es7)中新加入的提案,前端中的裝飾器的最早在Angular 2+中使用汉操,當(dāng)然再来,在當(dāng)時Angular中,裝飾器因TypeScript能使用磷瘤。
簡單來說芒篷,裝飾器是用一個代碼包裝另一個代碼的簡單方式。裝飾器是一個函數(shù)采缚,主要來修改類的或者類的方法行為针炉,只在類的范疇內(nèi)有用
如何使用JavaScript裝飾器
JavaScript中裝飾器使用特殊的語法,使用@作為標(biāo)識符扳抽,且放置在被裝飾代碼之前篡帕。
下面是一個簡單的裝飾器
function testable(target) {
target.UserName = 'linxiaodong';
}
@testable
class Test{
}
console.log(Test.UserName); //linxiaodong
給Test類天津愛了一個靜態(tài)屬性UserName.
裝飾器的作用
裝飾器的本質(zhì)只是一種語法糖而已,編譯時會把注解的代碼翻譯成我們熟悉的那種形式贸呢。
詳解
作用在方法上的 decorator 接收的第一個參數(shù)(target )是類的 prototype镰烧;如果把一個 decorator 作用到類上,則它的第一個參數(shù) target 是 類本身楞陷。
對類的修飾
上面的就是一個對類的修飾怔鳖,但是如果需要參數(shù)的話,我們可以在修飾器外面再封裝一層函數(shù)固蛾。
function testableWrap(username){
return function testable(target) {
target.UserName = username;
}
}
@testableWrap('linxiaodong')
class Test{
}
console.log(Test.UserName); //linxiaodong
上面是對類添加靜態(tài)方法结执,如果要給類的實例對象添加方法,可以通過prototype實現(xiàn)艾凯。
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
上面代碼通過修飾器mixins献幔,把Foo對象的方法添加到了MyClass的實例上面≈菏可以用Object.assign()模擬這個功能蜡感。
對類中方法的修飾
class Person {
@readonly
testFn() { return `${this.first} ${this.last}` }
}
console.log('Person.prototype',Person.prototype);
function readonly(target, name, descriptor){
console.log('target',target);
console.log('name',name); // testFn
console.log('descriptor',descriptor);
descriptor.writable = false;
return descriptor;
}
修飾器第一個參數(shù)是類的原型對象,上例是Person.prototype,修飾器的本意是要“修飾”類的實例铸敏,但是這個時候?qū)嵗€沒生成,所以只能去修飾原型(這不同于類的修飾悟泵,那種情況時target參數(shù)指的是類本身)杈笔;第二個參數(shù)是所要修飾的屬性名,第三個參數(shù)是該屬性的描述對象糕非。
修飾器不能用于函數(shù)
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
//SyntaxError: Leading decorators must be attached to a class declaration
由于存在函數(shù)提升蒙具,使得修飾器不能用于函數(shù)。類是不會提升的朽肥,所以就沒有這方面的問題禁筏。
另一方面,如果一定要修飾函數(shù)衡招,可以采用高階函數(shù)的形式直接執(zhí)行篱昔。
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const myDecorator = loggingDecorator(doSomething);
class Test{
@myDecorator('linxiaodong')
speak(){
console.log('speakFn');
}
}
// Starging
// Hello,linxiaodong
// Finished
常用的裝飾器
core-decorators.js是一個第三方模塊,提供了幾個常見的修飾器始腾,通過它可以更好地理解修飾器州刽。比如Angular2+中就內(nèi)置了這些裝飾器。
(1)@autobind
autobind修飾器使得方法中的this對象浪箭,綁定原始對象穗椅。
import { autobind } from 'core-decorators';
class Person {
@autobind
getPerson() {
return this;
}
}
let person = new Person();
let getPerson = person.getPerson;
getPerson() === person;
// true
使用原生 JS 實現(xiàn)裝飾器模式
// 首先我們要創(chuàng)建一個基類
function Man(){
this.def = 2;
this.atk = 3;
this.hp = 3;
}
// 裝飾者也需要實現(xiàn)這些方法,遵守 Man 的接口
Man.prototype={
toString:function(){
return `防御力:$,攻擊力:$,血量:$`;
}
}
// 創(chuàng)建裝飾器奶栖,接收 Man 對象作為參數(shù)匹表。
var Decorator = function(man){
this.man = man;
}
// 裝飾者要實現(xiàn)這些相同的方法
Decorator.prototype.toString = function(){
return this.man.toString();
}
// 繼承自裝飾器對象
// 創(chuàng)建具體的裝飾器,也是接收 Man 作對參數(shù)
var DecorateArmour = function(man){
var moreDef = 100;
man.def += moreDef;
Decorator.call(this,man);
}
DecorateArmour.prototype = new Decorator();
// 接下來我們要為每一個功能創(chuàng)建一個裝飾者對象宣鄙,重寫父級方法袍镀,添加我們想要的功能。
DecorateArmour.prototype.toString = function(){
return this.man.toString();
}
// 注意這里的調(diào)用方式
// 構(gòu)造器相當(dāng)于“過濾器”冻晤,面向切面的
var tony = new Man();
tony = new DecorateArmour(tony);
console.log(`當(dāng)前狀態(tài) ===> $`);
// 輸出:當(dāng)前狀態(tài) ===> 防御力:102,攻擊力:3,血量:3
下面是一個使用原生和使用裝飾器的寫法對比
// function decorateArmour(target, key, descriptor) {
// const method = descriptor.value;
// console.log(method);
// let moreDef = 100;
// let ret;
// descriptor.value = (...args)=>{
// args[0] += moreDef;
// ret = method.apply(target, args);
// return ret;
// }
// return descriptor;
// }
// class Man{
// constructor(def = 2,atk = 3,hp = 3){
// this.init(def,atk,hp);
// }
// @decorateArmour
// init(def,atk,hp){
// this.def = def; // 防御值
// this.atk = atk; // 攻擊力
// this.hp = hp; // 血量
// }
// toString(){
// return `防御力:${this.def},攻擊力:${this.atk},血量:${this.hp}`;
// }
// }
// var tony = new Man();
// console.log(`當(dāng)前狀態(tài) ===> ${tony.toString()}`);
// 使用原生來寫
// 首先我們要創(chuàng)建一個基類
function Man(){
this.def = 2;
this.atk = 3;
this.hp = 3;
}
// 裝飾者也需要實現(xiàn)這些方法流椒,遵守 Man 的接口
Man.prototype={
toString:function(){
return `防御力:${this.def},攻擊力:${this.atk},血量:${this.hp}`;
}
}
// 創(chuàng)建裝飾器,接收 Man 對象作為參數(shù)明也。
var Decorator = function(man){
this.man = man;
}
// 裝飾者要實現(xiàn)這些相同的方法
Decorator.prototype.toString = function(){
return this.man.toString();
}
// 繼承自裝飾器對象
// 創(chuàng)建具體的裝飾器宣虾,也是接收 Man 作對參數(shù)
var DecorateArmour = function(man){
var moreDef = 100;
man.def += moreDef;
Decorator.call(this,man);
}
DecorateArmour.prototype = new Decorator();
// 接下來我們要為每一個功能創(chuàng)建一個裝飾者對象,重寫父級方法温数,添加我們想要的功能绣硝。
DecorateArmour.prototype.toString = function(){
return this.man.toString();
}
// 注意這里的調(diào)用方式
// 構(gòu)造器相當(dāng)于“過濾器”,面向切面的
var tony = new Man();
tony = new DecorateArmour(tony);
console.log( `防御力:${tony.toString()}`);
// 輸出:當(dāng)前狀態(tài) ===> 防御力:102,攻擊力:3,血量:3
參考文獻(xiàn)
- Decorators in ES7:裝飾者模式讓你包裝已有的方法撑刺,從而擴展已有函數(shù)鹉胖。
- JavaScript設(shè)計模式:裝飾者模式:嚴(yán)重推薦,這一系列讓你比較透徹明白設(shè)計模式在 JS 中的應(yīng)用。
- ES7 之 Decorators 實現(xiàn) AOP 示例:如何實現(xiàn)一個簡單的 AOP甫菠。
- 細(xì)說 ES7 JavaScript Decorators:講解 ES7 Decorator的背后原理挠铲,就是使用了 Object.defineProperty 方法;
- How To Set Up the Babel Plugin in WebStorm:圖文并茂寂诱,教你如何設(shè)置 babel.
- Rest 參數(shù)和參數(shù)默認(rèn)值:ES6 為我們提供一種新的方式來創(chuàng)建可變參數(shù)的函數(shù)拂苹,Rest 參數(shù)和參數(shù)默認(rèn)值
- Exploring ES2016 Decorators:很完整的一個教程,里面涉及比較全面痰洒。
-
Traits with ES7 Decorators:相當(dāng)于是介紹
traits-decorator
模塊瓢棒; - ES6阮一峰
- ES7 Decorator 裝飾器 | 淘寶前端團(tuán)隊