1. 程式人生 > >JavaScript設計模式(一)

JavaScript設計模式(一)

Intro

一直很感興趣對於JavaScript這樣的動態型別語言,沒有強型別也沒有介面要怎麼通過設計模式寫出優雅的程式碼。這次一口氣讀完了騰訊出的一本關於JS設計模式的書,非常有啟發,這裡寫一個小的系列給大家分享。還是那句話,設計是為了更好的解決問題,而不是為了設計而設計,所以重要的是理解思想,而不是當成一個模板去套。畢竟設計模式會帶來效能的損耗和他人閱讀程式碼學習成本的上升,如非必要,勿增實體,度的把握才是我們在實踐中要去不斷探索的。

還有一些最近學習的心得也在上程式碼前先在這裡說一下:
1、分辨模式的關鍵是意圖而不是結構。
在設計模式的學習中,有人經常發出這樣的疑問:代理模式和裝飾者模式,策略模式和狀態模式,策略模式和智慧命令模式,這些模式的類圖看起來幾乎一模一樣,它們到底有什麼區別?實際上這種情況是普遍存在的,許多模式的類圖看起來都差不多,模式只有放在具體的環境下才有意義。比如我們的手機,把它當電話的時候,它就是電話;把它當鬧鐘的時候,它就是鬧鐘;用它玩遊戲的時候,它就是遊戲機。有很多模式的類圖和結構確實很相似,但這不太重要,辨別模式的關鍵是這個模式出現的場景,以及為我們解決了什麼問題。

2、JS的設計模式不應該是靜態語言設計模式的生拉硬套。
在JavaScript這種型別模糊的語言中,物件多型性是天生的,一個變數既可以指向一個類,又可以隨時指向另外一個類。JavaScript不存在型別耦合的問題,自然也沒有必要刻意去把物件“延遲”到子類建立,也就是說, JavaScript實際上是不需要工廠方法模式的。模式的存在首先是能為我們解決什麼問題,這種牽強的模擬只會讓人覺得設計模式既難懂又沒什麼用處。

3、duck typing
動態型別語言對變數型別的寬容給實際編碼帶來了很大的靈活性。由於無需進行型別檢測,我們可以嘗試呼叫任何物件的任意方法,而無需去考慮它原本是否被設計為擁有該方法。
這一切都建立在鴨子型別(duck typing)的概念上,鴨子型別的通俗說法是:“ 如果它走起路來像鴨子,叫起來也是鴨子,那麼它就是鴨子。鴨子型別指導我們只關注物件的行為,而不關注物件本身,也就是關注 HAS-A, 而不是 IS-A。

4、基於鴨子型別的動態型別語言的面向物件設計
鴨子型別的概念至關重要。利用鴨子型別的思想,我們不必藉助超型別的幫助,就能輕鬆地在動態型別語言中實現一個原則:“面向介面程式設計,而不是面向實現程式設計”。例如,一個物件若有 push 和 pop 方法,並且這些方法提供了正確的實現,它就可以被當作棧來使用。一個物件如果有 length 屬性,也可以依照下標來存取屬性(最好還要擁
有 slice 和 splice 等方法),這個物件就可以被當作陣列來使用。
在靜態型別語言中,要實現“面向介面程式設計”並不是一件容易的事情,往往要通過抽象類或者介面等將物件進行向上轉型。當物件的真正型別被隱藏在它的超型別身後,這些物件才能在型別檢查系統的“監視”之下互相被替換使用。只有當物件能夠被互相替換使用,才能體現出物件多型性的價值。
“面向介面程式設計”是設計模式中最重要的思想,但在 JavaScript 語言中,“面向介面程式設計”的過程跟主流的靜態型別語言不一樣,因此,在 JavaScript 中實現設計模式的過程與在一些我們熟悉的語言中實現的過程會大相徑庭。

PS:本文預設你已經是一個JS熟手,對於JS的this,call,apply,原型,閉包等重要概念已經有起碼的理解,如果不太熟悉,建議學習後再來嘗試閱讀本文。另外本文核心關注的是JS怎樣實現對應的模式,所以不再會贅述設計模式本身的相關知識,有需要的同學可以去參考對應的書籍。

一、Singleton

//惰性載入模式,例項在需要的時候才會建立
var getSingle = function( fn ){
    var result;
    return function(){
        return result || ( result = fn .apply(this, arguments ));
    }
};

Example:

//JQuery
var bindEvent = function(){
    $( 'div' ).one( 'click', function()
    {
        alert ( 'click' );
    });
};

//Our Impl
var bindEvent = getSingle(function()
{
    document.getElementById( 'div1' ).onclick = function()
    {
        alert ( 'click' );
    }

    //注意這裡必須有返回
    return true;
});

在 getSinge 函式中,實際上也提到了閉包和高階函式的概念。單例模式是一種簡單但非常實用的模式,特別是惰性單例技術,在合適的時候才建立物件,並且只建立唯一的一個。更奇妙的是,建立物件和管理單例的職責被分佈在兩個不同的方法中,這兩個方法組合起來才具有單例模式的威力。

二、Strategy

var StrategyA = function (data)
{
    console.log("strategy A dispose " + data);
}

var StrategyB = function (data)
{
    console.log("strategy B dispose " + data);
}

var StrategyC = function (data)
{
    console.log("strategy C dispose " + data);
}

var Invoker = function (strategy, data)
{
    return strategy(data);
}

var context = function (data)
{
    if(data > 3)
    {
        return new Invoker(StrategyA, data);
    }
    else if(data > 1 && data <= 3)
    {
        return new Invoker(StrategyB, data);
    }
    else
    {
        return new Invoker(StrategyC, data);
    }
}

context(5);

Peter Norvig 在他的演講中曾說過:“在函式作為一等物件的語言中,策略模式是隱形的。strategy 就是值為函式的變數。”在JavaScript 中,除了使用類來封裝演算法和行為之外,使用函式當然也是一種選擇。這些“演算法”可以被封裝到函式中並且四處傳遞,也就是我們常說的“高階函式”。實際上在 JavaScript 這種將函式作為一等物件的語言裡,策略模式已經融入到了語言本身當中,我們經常用高階函式來封裝不同的行為,並且把它傳遞到另一個函式中。當我們對這些函式發出“呼叫”的訊息時,不同的函式會返回不同的執行結果。在 JavaScript 中,”函式物件的多型性“來的更加簡單。所以,在 JavaScript 語言的策略模式中,策略類往往被函式所代替,這時策略模式就成為一種“隱形”的模式。

三、Proxy

var before = function (fn, fnBefore)
{
    var self = this;

    return function ()
    {
        fnBefore.apply(self, arguments);
        return fn.apply(self, arguments);
    }
}

var after = function (fn, fnAfter)
{
    var self = this;

    return function ()
    {
        var ret = fn.apply(self, arguments);
        fnAfter.apply(self, arguments);
        return ret;
    }
}

var test = function ()
{
    console.log("test");
}

test = before(test, function ()
{
    console.log("before");
});

test = after(test, function ()
{
    console.log("after");
});

test();

這裡我們直接引入了JS的AOP,代理模式是一個非常強大的模式,雖然它的原理很簡單。幾乎所有的hack技術,本質都是proxy一個原系統中的類或方法,然後植入自己想要做的事情。其他的proxy模式應用,一般需要通過具體的應用場景來實現,這裡就不再一一列舉。

四、Iterator

//內部迭代器
var each = function (arr, callback)
{
    for (var i = 0; i < arr.length; i++)
    {
        callback.call(arr[i], i, arr[i]);
    }
}

each([1, 2, 3], function (i, n)
{
    console.log([i, n]);
});

//外部迭代器,必須顯式地請求迭代下一個元素,可以手工控制迭代過程和順序
var Iterator = function (obj)
{
    var current = 0;

    var next = function ()
    {
        current += 1;
    }

    var isDone = function ()
    {
        return current >= obj.length;
    }

    var getCurrentItem = function ()
    {
        return obj[current];
    }

    return {
        next : next,
        isDone : isDone,
        getCurrentItem : getCurrentItem
    }
}

var compare = function (iterator1, iterator2)
{
    while (!iterator1.isDone() && !iterator2.isDone())
    {
        if(iterator1.getCurrentItem() != iterator2.getCurrentItem())
        {
            throw new Error("not equals");
        }

        iterator1.next();
        iterator2.next();
    }

    alert("equals");
}

var iterator1 = Iterator([1, 2, 3]);
var iterator2 = Iterator([1, 2, 4]);

compare(iterator1, iterator2);

迭代器模式是一種相對簡單的模式,簡單到很多時候我們都不認為它是一種設計模式。目前的絕大部分語言都內建了迭代器。

五、Observer

var Observable = (function ()
{
    var global = this;
    var Observable;
    var _default = "default";

    Observable = (function ()
    {
        var _addListener;
        var _notify;
        var _removeListener;
        var _slice = Array.prototype.slice;
        var _shift = Array.prototype.shift;
        var _unshift = Array.prototype.unshift;
        var namespaceCache = {};
        var _create;
        var find;
        var each;

        each = function (arr, fn)
        {
            var ret;

            for(var i = 0; i < arr.length; i++)
            {
                var n = arr[i];
                ret = fn.call(n, i , n);
            }

            return ret;
        };

        _addListener = function (key, listener, cache)
        {
            if(!cache[key])
            {
                cache[key] = [];
            }

            cache[key].push(listener);
        };

        _removeListener = function (key, cache, listener)
        {
            if(cache[key])
            {
                if(listener)
                {
                    for(var i = 0; i < cache[key].length; i++)
                    {
                        if(listener === cache[key][i])
                        {
                            cache[key].splice(i, 1);
                        }
                    }
                }
                else
                {
                    cache[key] = [];
                }
            }
        };

        _notify = function ()
        {
//                    var obs = _shift.call(arguments);
//                    var _arguments = _shift.call(arguments);

            var cache = _shift.call(arguments);
            var key = _shift.call(arguments);
            var args = arguments;

            var _self = this;
            var listeners = cache[key];

            if(!listeners || !listeners.length)
            {
                return;
            }


            return each(listeners, function ()
            {
                //this refer to a listener
                return this.apply(_self, args);
            });
        };

        _create = function (namespace)
        {
            var namespace = namespace || _default;
            var cache = {};
            var offlineStack = [];

            var ret = {
                addListener : function (key, listener, last)
                {
                    _addListener(key, listener, cache);

                    if(offlineStack === null)
                    {
                        return;
                    }

                    if(last === "last")
                    {
                        offlineStack.length && offlineStack.pop();
                    }
                    else
                    {
                        each(offlineStack, function ()
                        {
                            this();
                        });
                    }

                    offlineStack = null;
                },

                //make the key of the listeners with the only one
                one : function (key, listener, last)
                {
                    _removeListener(key, cache);
                    this.addListener(key, listener, last);
                },

                removeListener : function (key, listener)
                {
                    _removeListener(key, cache, listener);
                },

                notify : function ()
                {
                    var fn;
                    var args;
                    var _self = this;

                    _unshift.call(arguments, cache);
                    args = arguments;

                    fn = function ()
                    {
                        return _notify.apply(_self, args);
                    }

                    if(offlineStack)
                    {
                        return offlineStack.push(fn);
                    }
                    return fn();
                },
            };

            return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
        };

        return {
            create : _create,

            one : function (key, listener, last)
            {
                var observable = this.create();
                observable.one(key, listener, last);
            },

            addListener : function (key, listener, last)
            {
                var observable = this.create();
                observable.addListener(key, listener, last);
            },

            removeListener : function (key, listener)
            {
                var observable = this.create();
                observable.removeListener(key, listener);
            },

            notify : function ()
            {
                var observable = this.create();
                observable.notify.apply(this, arguments);
            }
        };


    })();

    return Observable;

})();

Observable.notify("msg", 1000);
Observable.addListener("msg", function (data)
{
    alert("final msg " + data);
});

其實看書前我也沒想到靜態語言中非常簡單的觀察者模式在js裡可以寫這麼複雜,不過這個實現方法是同時考慮了“離線訊息”的,也就是說框架可以先發布訊息,然後後來的訂閱者一樣可以收到訊息。實際開發中如果不存在這樣的業務可以把這個程式碼做一個簡化。

這篇先說到這。