js之裝飾者模式

裝飾者模式

在程序開發中,許多時候都並不希望某個類天生就非常龐大,一次性包含許多職責。那麼我們就可以使用裝飾者模式。裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。


level01:模擬傳統面嚮對象語言的裝飾者模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        var Plane = function(){}
        Plane.prototype.fire = function(){
            console.log( '發射普通子彈' );
        }
//        接下來增加兩個裝飾類,分別是導彈和原子彈:
        var MissileDecorator = function( plane ){
            this.plane = plane;
        }
        MissileDecorator.prototype.fire = function(){
            this.plane.fire();
            console.log( '發射導彈' );
        }
        var AtomDecorator = function( plane ){
            this.plane = plane;
        }
        AtomDecorator.prototype.fire = function(){
            this.plane.fire();
            console.log( '發射原子彈' );
        }
        var plane = new Plane();
        plane = new MissileDecorator( plane );
        plane = new AtomDecorator( plane );
        plane.fire();
        // 分別輸出: 發射普通子彈、發射導彈、發射原子彈

    </script>
</body>
</html>

導彈類和原子彈類的構造函數都接受參數 plane 對象,並且保存好這個參數,在它們的 fire方法中,除了執行自身的操作之外,還調用 plane 對象的 fire 方法。

這種給對象動態增加職責的方式,並沒有真正地改動對象自身,而是將對象放入另一個對象之中,這些對象以一條鏈的方式進行引用,形成一個聚合對象。這些對象都擁有相同的接口( fire
方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操作,隨後把請求轉發給鏈中的
下一個對象。

因爲裝飾者對象和它所裝飾的對象擁有一致的接口,所以它們對使用該對象的客戶來說是透明的,被裝飾的對象也並不需要了解它曾經被裝飾過,這種透明性使得我們可以遞歸地嵌套任意多個裝飾者對象。


level02:回到 JavaScript 的裝飾者

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        var plane = {
            fire: function(){
                console.log( '發射普通子彈' );
            }
        }
        var missileDecorator = function(){
            console.log( '發射導彈' );
        }
        var atomDecorator = function(){
            console.log( '發射原子彈' );
        }
        var fire1 = plane.fire;
        plane.fire = function(){
            fire1();
            missileDecorator();
        }
        //前者plane.fire()嵌套在後者plane.fire()裏
        var fire2 = plane.fire;
        plane.fire = function(){
            fire2();
            atomDecorator();
        }
        plane.fire();
        // 分別輸出: 發射普通子彈、發射導彈、發射原子彈
    </script>
</body>
</html>

level01:裝飾函數

var a = function(){
    alert (1);
}
// 改成:
var a = function(){
    alert (1);
    alert (2);
}

缺點:直接違反了開放封閉原則。


level02:裝飾函數

很多時候我們不想去碰原函數,也許原函數是由其他同事編的,裏面的實現非常雜亂。甚至在一個古老的項目中,這個函數的源代碼被隱藏在一個我們不願碰觸的陰暗角落裏。現在需要一個辦法,在不改變函數源代碼的情況下,能給函數增加功能,這正是開放-封閉原則給我們指出的光明道路。

var a = function(){
    alert (1);
}
var _a = a;
a = function(){
    _a();
    alert (2);
}
a();

這是實際開發中很常見的一種做法,比如我們想給 window 綁定 onload 事件,但是又不確定這個事件是不是已經被其他人綁定過,爲了避免覆蓋掉之前的 window.onload 函數中的行爲,我
們一般都會先保存好原先的 window.onload ,把它放入新的window.onload 裏執行:

window.onload = function(){
    alert (1);
}
var _onload = window.onload || function(){};
window.onload = function(){
    _onload();
    alert (2);
}

這樣的代碼當然是符合開放封閉原則的,我們在增加新功能的時候,確實沒有修改原來的window.onload 代碼,但是這種方式存在以下兩個問題。

  1. 必須維護 _onload 這個中間變量,雖然看起來並不起眼,但如果函數的裝飾鏈較長,或者需要裝飾的函數變多,這些中間變量的數量也會越來越多。
  2. 其實還遇到了 this 被劫持的問題,在 window.onload 的例子中沒有這個煩惱,是因爲調用
    普通函數 _onload 時, this 也指向 window ,跟調用 window.onload 時一樣(函數作爲對象的方法被調用時, this 指向該對象,所以此處 this 也只指向 window )。

在把 window.onload換成 document.getElementById ,代碼如下:

var _getElementById = document.getElementById;
document.getElementById = function( id ){
    alert (1);
    return _getElementById( id ); // (1)
}
var button = document.getElementById( 'button' );

執行這段代碼,我們看到在彈出 alert(1) 之後,緊接着控制檯拋出了異常:// 輸出: Uncaught TypeError: Illegal invocation

異常發生在(1) 處的 _getElementById( id ) 這句代碼上,此時 _getElementById 是一個全局函數,當調用一個全局函數時, this 是指向 window 的,而 document.getElementById 方法的內部實現需要使用 this 引用, this 在這個方法內預期是指向 document ,而不是 window , 這是錯誤發生的原因,所以使用現在的方式給函數增加功能並不保險。

改進後的代碼可以滿足需求,我們要手動把 document 當作上下文 this 傳入 _getElementById :

<html>
    <button id="button"></button>
<script>
    var _getElementById = document.getElementById;
    document.getElementById = function(){
        alert (1);
        return _getElementById.apply( document, arguments );
    }
    var button = document.getElementById( 'button' );
</script>
</html>

裝飾者模式和代理模式

裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣爲對象提供一定程度上的間接引用,它們的實現部分都保留了對另外一個對象的引用,並且向那個對象發送請求。

代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當接訪問本體不方便或者不符合需要時,爲這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是爲對象動態加入行爲。換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係可以靜態的表達,也就是說,這種關係在一開始就可以被確定。而裝飾者模式用於一開始不能確定對象的全部功能時。代理模式通常只有一層代理本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈。

在虛擬代理實現圖片預加載的例子中,本體負責設置 img 節點的 src,代理則提供了預加載的功能,這看起來也是“加入行爲”的一種方式,但這種加入行爲的方式和裝飾者模式的偏重點
是不一樣的。裝飾者模式是實實在在的爲對象增加新的職責和行爲,而代理做的事情還是跟本體一樣,最終都是設置 src。但代理可以加入一些“聰明”的功能,比如在圖片真正加載好之前,
先使用一張佔位的 loading圖片反饋給客戶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章