什麼是裝飾者模式
裝飾者模式是一種爲函數或類增添特性的技術,它可以讓我們在不修改原來對象的基礎上,爲其增添新的能力和行爲。它本質上也是一個函數(在javascipt中,類也只是函數的語法糖)。
我們什麼時候可以弄到它呢
我們來假設一個場景,一個自行車商店有幾種型號的自行車,現在商店允許用戶爲每一種自行車提供一些額外的配件,比如前燈、尾燈、鈴鐺等。每選擇一種或幾種配件都會影響自行車的售價。
如果按照比較傳統的創建子類的方式,就等於我們目前有一個自行車基類,而我們要爲每一種可能的選擇創建一個新的類。可是由於用戶可以選擇一種或者幾種任意的配件,這就導致最終可能會生產幾十上百個子類,這明顯是不科學的。然而,對這種情況,我們可以使用裝飾者模式來解決這個問題。
自行車的基類如下:
class Bicycle {
// 其它方法
wash () {}
ride () {}
getPrice() {
return 200;
}
}
那麼我們可以先創建一個裝飾者模式基類
class BicycleDecotator {
constructor(bicycle) {
this.bicycle = bicycle;
}
wash () {
return this.bicycle.wash();
}
ride () {
return this.bicycle.ride();
}
getPrice() {
return this.bicycle.getPrice();
}
}
這個基類其實沒有做什麼事情,它只是接受一個Bicycle實例,實現其對應的方法,並且將調用其方法返回而已。
有了這個基類之後,我們就可以根據我們的需求對原來的Bicycle類爲所欲爲了。比如我可以創建一個添加了前燈的裝飾器以及添加了尾燈的裝飾器:
class HeadLightDecorator extends BicycleDecorator {
constructor(bicycle) {
super(bicycle);
}
getPrice() {
return this.bicycle.getPrice() + 20;
}
}
class TailLightDecorator extends BicycleDecorator {
constructor(bicycle) {
super(bicycle);
}
getPrice() {
return this.bicycle.getPrice() + 20;
}
}
那麼,接下來我們就可以來對其自由組合了:
let bicycle = new Bicycle();
console.log(bicycle.getPrice()); // 200
bicycle = new HeadLightDecorator(bicycle); // 添加了前燈的自行車
console.log(bicycle.getPrice()); // 220
bicycle = new TailLightDecorator(bicycle); // 添加了前燈和尾燈的自行車
console.log(bicycle.getPrice()); // 240
這樣寫的好處是什麼呢?假設說我們有10個配件,那麼我們只需要寫10個配件裝飾器,然後就可以任意搭配成不同配件的自行車並計算價格。而如果是按照子類的實現方式的話,10個配件可能就需要有幾百個甚至上千個子類了。
從例子中我們可以看出裝飾者模式的適用場合:
- 如果你需要爲類增添特性或職責,可是從類派生子類的解決方法並不太現實的情況下,就應該使用裝飾者模式。
- 在例子中,我們並沒有對原來的Bicycle基類進行修改,因此也不會對原有的代碼產生副作用。我們只是在原有的基礎上增添了一些功能。因此,如果想爲對象增添特性又不想改變使用該對象的代碼的話,則可以採用裝飾者模式。
裝飾者模式除了可以應用在類上之外,還可以應用在函數上(其實這就是高階函數)。比如,我們想測量函數的執行時間,那麼我可以寫這麼一個裝飾器:
function func() {
console.log('func');
}
function timeProfileDecorator(func) {
return function (...args) {
const startTime = new Date();
func.call(this, ...args);
const elapserdTime = (new Date()).getTime() - startTime.getTime();
console.log(`該函數消耗了${elapserdTime}ms`);
}
}
const newFunc = timeProfileDecorator(func);
console.log(newFunc());
做一些有趣的事情
既然知道了裝飾者模式可以在不修改原來代碼的情況下爲其增添一些新的功能,那麼我們就可以來做一些有趣的事情。
我們可以爲一個類的方法提供性能分析的功能。
class TimeProfileDecorator {
constructor(component, keys) {
this.component = component;
this.timers = {};
const self = this;
for (let i in keys) {
let key = keys[i];
if (typeof component[key] === 'function') {
this[key] = function(...args) {
this.startTimer(key);
// 解決this引用錯誤問題
component[key].call(component, ...args);
this.logTimer(key);
}
}
}
}
startTimer(namespace) {
this.timers[namespace] = new Date();
}
logTimer(namespace) {
const elapserdTime = (new Date()).getTime() - this.timers[namespace].getTime();
console.log(`該函數消耗了${elapserdTime}ms`);
}
}
// example
class Test {
constructor() {
this.name = 'cjg';
this.age = 22;
}
sayName() {
console.log(this.name);
}
sayAge() {
console.log(this.age);
}
}
let test1 = new Test();
test1 = new TimeProfileDecorator(test1, ['sayName', 'sayAge']);
console.log(test1.sayName());
console.log(test1.sayAge());
對函數進行增強
節流函數or防抖函數
function throttle(func, delay) {
const self = this;
let tid;
return function(...args) {
if (tid) return;
tid = setTimeout(() => {
func.call(self, ...args);
tid = null;
}, delay);
}
}
function debounce(func, delay) {
const self = this;
let tid;
return function(...args) {
if (tid) clearTimeout(tid);
tid = setTimeout(() => {
func.call(self, ...args);
tid = null;
}, delay);
}
}
緩存函數返回值
// 緩存函數結果,對於一些計算量比較大的函數效果比較明顯。
function memorize(func) {
const cache = {};
return function (...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('緩存了');
return cache[key];
}
const result = func.call(this, ...args);
cache[key] = result;
return result;
};
}
function fib(num) {
return num < 2 ? num : fib(num - 1) + fib(num - 2);
}
const enhanceFib = memorize(fib);
console.log(enhanceFib(40));
console.log(enhanceFib(40));
console.log(enhanceFib(40));
console.log(enhanceFib(40));
構造React高階組件,爲組件增加額外的功能,比如爲組件提供shallowCompare功能:
import React from 'react';
const { Component } = react;
const ShadowCompareDecorator = (Instance) => class extends Component {
shouldComponentUpdate(nextProps, nextState) {
return !shallowCompare(this.props, nextProps) ||
!shallowCompare(this.state, nextState);
}
render() {
return (
<Instance {...this.props} />
);
}
};
export default ShadowCompareDecorator;
當然,你如果用過react-redux的話,你肯定也用過connect。其實connect也是一種高階組件的方式。它通過裝飾者模式,從Provider的context裏拿到全局的state,並且將其通過props的方式傳給原來的組件。
總結
使用裝飾者模式可以讓我們爲原有的類和函數增添新的功能,並且不會修改原有的代碼或者改變其調用方式,因此不會對原有的系統帶來副作用。我們也不用擔心原來系統會因爲它而失靈或者不兼容。就我個人而言,我覺得這是一種特別好用的設計模式。
一個好消息就是,js的裝飾器已經加入了es7的草案裏啦。它讓我們可以更加優雅的使用裝飾者模式,如果有興趣的可以添加下babel的plugins插件提前體驗下。阮一峯老師的這個教程也十分淺顯易懂。
參考文獻:
Javascript設計模式