最近打算系統(tǒng)的學(xué)習(xí)javascript設(shè)計(jì)模式,以便自己在開發(fā)中遇到問題可以按照設(shè)計(jì)模式提供的思路進(jìn)行封裝,這樣可以提高開發(fā)效率并且可以預(yù)先規(guī)避很多未知的問題。

先從最基本的單例模式開始。

什么是單例模式:

單例模式,從名字拆分來看,單指的是一個(gè),例是實(shí)例,意思是說多次通過某個(gè)類創(chuàng)造出來實(shí)例始終只返回同一個(gè)實(shí)例,它限制一個(gè)類只能有一個(gè)實(shí)例。單例模式主要是為了解決對(duì)象的創(chuàng)建問題。單例模式的特點(diǎn):

  1. 一個(gè)類只有一個(gè)實(shí)例

  2. 對(duì)外提供唯一的訪問接口

在一些以類為核心的語言中,例如java,每創(chuàng)建一個(gè)對(duì)象就必須先定義一個(gè)類,對(duì)象是從類創(chuàng)建而來。js是一門無類(class-free)的語言,在js中創(chuàng)建對(duì)象的方法非常簡單,不需要先定義類即可創(chuàng)建對(duì)象。

在js中,單例模式是一種常見的模式,例如瀏覽器中提供的window對(duì)象,處理數(shù)字的Math對(duì)象。

單例模式的實(shí)現(xiàn)

1. 對(duì)象字面量

在js中實(shí)現(xiàn)單例最簡單的方式是創(chuàng)建對(duì)象字面量,字面量對(duì)象中可以包含多個(gè)屬性和方法。

var mySingleton = {
    attr1:1,
    attr2:2,
    method:function (){        console.log("method");  
    }
}

以上創(chuàng)建一個(gè)對(duì)象,放在全局中,就可以在任何地方訪問,要訪問對(duì)象中的屬性和方法,必須通過mySingleton這個(gè)對(duì)象,也就是說提供了唯一一個(gè)訪問接口。

2. 使用閉包私有化

擴(kuò)展mySingleton對(duì)象,添加私有的屬性和方法,使用閉包的形式在其內(nèi)部封裝變量和函數(shù)聲明,只暴露公共成員和方法。

var mySingleton = (function (){    //私有變量
    var privateVal = '我是私有變量';    //私有函數(shù)
    function privateFunc(){        console.log('我是私有函數(shù)');  
    }    return {
            attr1:1,
            attr2:2,
            method:function (){                console.log("method");  
                privateFunc();
            }
        }   
})()

privateValprivateVal被封裝在閉包產(chǎn)生的作用域中,外界訪問不到這兩個(gè)變量,這避免了對(duì)全局命名污染。

3.惰性單例

無論使用對(duì)象字面量或者閉包私有化的方式創(chuàng)建單例,都是在腳本一加載就被創(chuàng)建。有時(shí)候頁面可能不會(huì)用到這個(gè)單例對(duì)象,這樣就會(huì)造成資源浪費(fèi)。對(duì)于這種情況,最佳處理方式是使用惰性單例,也就是在需要這個(gè)單例對(duì)象時(shí)再初始化。

var mySingleton = (function (){    function init(){        //私有變量
        var privateVal = '我是私有變量';        //私有函數(shù)
        function privateFunc(){            console.log('我是私有函數(shù)');  
        }        return {
            attr1:1,
            attr2:2,
            method(){                console.log("method");  
                privateFunc();
            }
        }
    }    //用來保存創(chuàng)建的單例對(duì)象
     var instance = null;    return {
        getInstance (){            //instance沒有存值,就執(zhí)行函數(shù)得到對(duì)象
            if(!instance){
                instance = init();
            }   
            //instance存了值,就返回這個(gè)對(duì)象
            return instance;
        }
    }
})();//得到單例對(duì)象var singletonObj1 = mySingleton.getInstance();var singletonObj2 = mySingleton.getInstance();console.log( singletonObj1 === singletonObj2 ); //true

程序執(zhí)行后,將創(chuàng)建單例對(duì)象的代碼封裝到init函數(shù)中,只暴露了獲取單例對(duì)象的函數(shù)getInstance。當(dāng)有需要用到時(shí),通過調(diào)用函數(shù)mySingleton.getInstance()得到單例對(duì)象,同時(shí)使用instance將對(duì)象緩存起來,再次調(diào)用mySingleton.getInstance()后得到的是同一個(gè)對(duì)象,這樣通過一個(gè)函數(shù)不會(huì)創(chuàng)建多個(gè)對(duì)象,起到節(jié)省資源的目的。

4. 使用構(gòu)造函數(shù)

可以使用構(gòu)造函數(shù)的方式,創(chuàng)造單例對(duì)象:

function mySingleton(){    //如果緩存了實(shí)例,則直接返回
    if (mySingleton.instance) {        return mySingleton.instance;
    }    //當(dāng)?shù)谝淮螌?shí)例化時(shí),先緩存實(shí)例
    mySingleton.instance = this;

}

mySingleton.prototype.otherFunc = function (){    console.log("原型上其他方法"); 
}var p1 = new mySingleton();var p2 = new mySingleton();console.log( p1 === p2 );  //true

當(dāng)?shù)谝淮问褂?em>new調(diào)用函數(shù)創(chuàng)建實(shí)例時(shí),通過函數(shù)的靜態(tài)屬性mySingleton.instance把實(shí)例緩存起來,在第二次用new調(diào)用函數(shù),判斷實(shí)例已經(jīng)緩存過了,直接返回,那么第一次得到的實(shí)例p1和第二次得到的實(shí)例p2是同一個(gè)對(duì)象。這樣符合單例模式的特點(diǎn):一個(gè)類只能有一個(gè)實(shí)例。

這樣做有一個(gè)問題,暴露了可以訪問緩存實(shí)例的屬性mySingleton.instance,這個(gè)屬性的值可以被改變:

var p1 = new mySingleton();//改變mySingleton.instance的值//mySingleton.instance = null;//或者mySingleton.instance = {};var p2 = new mySingleton();console.log( p1 === p2 );  //false

改變了mySingleton.instance值后,再通過new調(diào)用構(gòu)造函數(shù)創(chuàng)建實(shí)例時(shí),又會(huì)重新創(chuàng)建新的對(duì)象,那么p1p2就不是同一個(gè)對(duì)象,違反了單例模式一個(gè)類只能有一個(gè)實(shí)例。

閉包中的實(shí)例

不使用函數(shù)的靜態(tài)屬性緩存實(shí)例,而是重新改寫構(gòu)造函數(shù):

function mySingleton(){    //緩存當(dāng)前實(shí)例
    var instance  = this;    //執(zhí)行完成后改寫構(gòu)造函數(shù)
    mySingleton = function (){        return instance;    
    }    //其他的代碼
    instance.userName = "abc";

}

mySingleton.prototype.otherFunc = function (){    console.log("原型上其他方法"); 
}var p1 = new mySingleton();var p2 = new mySingleton();console.log( p1 === p2 );  //true

第一次使用new調(diào)用函數(shù)創(chuàng)建實(shí)例后,在函數(shù)中創(chuàng)建instance用來緩存實(shí)例,把mySingleton改寫為另一個(gè)函數(shù)。如果再次使用new調(diào)用函數(shù)后,利用閉包的特性,返回了緩存的對(duì)象,所以p1p2是同一個(gè)對(duì)象。

這樣雖然也可以保證一個(gè)類只返回一個(gè)實(shí)例,但注意,第二次再次使用new調(diào)用的構(gòu)造函數(shù)是匿名函數(shù),因?yàn)閙ySingleton已經(jīng)被改寫:

//第二次new mySingleton()時(shí)這個(gè)匿名函數(shù)才是真正的構(gòu)造函數(shù)mySingleton = function (){    return instance;    
}

再次給原mySingleton.prototype上添加是屬性,實(shí)際上這是給匿名函數(shù)的原型添加了屬性:

var p1 = new mySingleton();//再次給mySingleton的原型上添加屬性mySingleton.prototype.addAttr = "我是新添加的屬性";var p2 = new mySingleton();console.log(p2.addAttr); //undefined

對(duì)象p2訪問屬性addAttr并沒有找到。通過一個(gè)構(gòu)造函數(shù)構(gòu)造出來的實(shí)例并不能訪問原型上的方法或?qū)傩裕@是一種錯(cuò)誤的做法,還需要繼續(xù)改進(jìn)。

function mySingleton(){        
    var instance;    //改寫構(gòu)造函數(shù)
    mySingleton = function (){        return instance;    
    }    //把改寫后構(gòu)造函數(shù)的原型指向this
    
    mySingleton.prototype = this;    //constructor改寫為改寫后的構(gòu)造函數(shù)
    mySingleton.prototype.constructor = mySingleton;    //得到改寫后構(gòu)造函數(shù)創(chuàng)建的實(shí)例
    instance = new mySingleton;    //其他的代碼
    instance.userName = "abc";    //顯示的返回改寫后構(gòu)造函數(shù)創(chuàng)建的實(shí)例
    return instance;

}

mySingleton.prototype.otherFunc = function (){    console.log("原型上其他方法"); 
}var p1 = new mySingleton();//再次給mySingleton的原型上添加屬性mySingleton.prototype.addAttr = "我是新添加的屬性";var p2 = new mySingleton();console.log(p2.addAttr); //'我是新添加的屬性'console.log( p1 === p2 );  //true

以上代碼主要做了以下幾件事:

  1. 改寫mySingleton函數(shù)為匿名函數(shù)

  2. 改寫mySingleton的原型為第一次通過new創(chuàng)建的實(shí)例

  3. 因?yàn)楦膶懥藀rototype,要把constructor指回mySingleton

  4. 顯式返回通過改寫后mySingleton構(gòu)造函數(shù)構(gòu)造出的實(shí)例

無論使用多少次new調(diào)用mySingleton這個(gè)構(gòu)造函數(shù),都返回同一個(gè)對(duì)象,并且這些對(duì)象都共享同一個(gè)原型。

實(shí)踐單例模式

1. 使用命名空間

根據(jù)上述,在js中創(chuàng)建一個(gè)對(duì)象就是一個(gè)單例,把一類的方法和屬性放在對(duì)象中,都通過提供的全局對(duì)象訪問。

var mySingleton = {
    attr1:1,
    attr2:2,
    method:function (){        console.log("method");  
    }
}

這樣的方式耦合度極高,例如:要給這個(gè)對(duì)象添加屬性:

mySingleton.width = 1000;  //添加一個(gè)屬性//添加一個(gè)方法會(huì)覆蓋原有的方法mySingleton.method = function(){};

如果在多人協(xié)作中,這樣添加屬性的方式經(jīng)常出現(xiàn)被覆蓋的危險(xiǎn),可以采用命名空間的方式解決。

//A同學(xué)mySingleton.a = {};
mySingleton.a.method = function(){}//訪問mySingleton.a.method();//B同學(xué)mySingleton.b = {};
mySingleton.b.method = function(){}//訪問mySingleton.b.method();

都在自己的命名空間中,覆蓋的幾率會(huì)很小。
可以封裝一個(gè)動(dòng)態(tài)創(chuàng)建命名空間的通用方法,這樣在需要獨(dú)立的命名空間時(shí)只需要調(diào)用函數(shù)即可。

mySingleton.namespace = function(name){    var arr = name.split(".");    //存一下對(duì)象
    var currentObj = mySingleton;    for( var i = 0; i < arr.length; i++ ){        //如果對(duì)象中不存在,則賦值添加屬性
        if(!currentObj[arr[i]]){
            currentObj[arr[i]] = {};
        }        //把變量重新賦值,便于循環(huán)繼續(xù)創(chuàng)建命名空間
        currentObj = currentObj[arr[i]]
    }
}//創(chuàng)建命名空間mySingleton.namespace("bom");
mySingleton.namespace("dom.style");

以上調(diào)用函數(shù)生成命名空間的方式代碼等價(jià)于:

mySingleton.bom = {};
mySingleton.dom = {};
mySingleton.dom.style = {};

2. 單例登錄框

使用面向?qū)ο髮?shí)現(xiàn)一個(gè)登錄框,在點(diǎn)擊登錄按鈕后登錄框被append到頁面中,點(diǎn)擊關(guān)閉就將登錄框從頁面中remove掉,這樣頻繁的操作DOM不合理也不是必要的。

只需要在點(diǎn)擊關(guān)閉時(shí)隱藏登錄框,再次點(diǎn)擊按鈕后,只需要show出來即可。

頁面中只放一個(gè)按鈕:

<input type="button" value="登錄" id="loginBtn" />

js實(shí)現(xiàn):

function Login(){    var instance;

    Login = function(){        return install;
    }

    Login.prototype = this;


    install = new Login;

    install.init();    return install;
}

Login.prototype.init = function(){    //得到登錄框元素
    this.Login = this.createHtml();    document.body.appendChild(this.Login);    //綁定事件
    this.addEvent();
}
Login.prototype.createHtml = function(){    var LoginDiv = document.createElement("div");
    LoginDiv.className = "box";    var html = `<input type="button" value="關(guān)閉彈框" class="close" /><p>這里做登錄</p>`

    LoginDiv.innerHTML = html;    return LoginDiv;
}
Login.prototype.addEvent = function(){    var close = this.Login.querySelector(".close");    var _this = this;
    close.addEventListener("click",function(){
        _this.Login.style.display = 'none';
    })
}
Login.prototype.show = function(){    this.Login.style.display = 'block';
}//點(diǎn)擊頁面中的按鈕var loginBtn = document.querySelector("#loginBtn");
loginBtn.onclick = function(){    var login = new Login();    //每次讓登錄框出現(xiàn)即可
    login.show();
}

上面的代碼根據(jù)單例模式的使用構(gòu)造函數(shù)來實(shí)現(xiàn)的。這樣在一開始生成了一個(gè)對(duì)象,之后使用的都是同一個(gè)對(duì)象。

總結(jié)

單例模式是一種非常實(shí)用的模式,特別是懶性單例技術(shù),在合適時(shí)候創(chuàng)建對(duì)象,并且只創(chuàng)建唯一一個(gè),這樣減少不必要的內(nèi)存消耗。

http://www.cnblogs.com/floatboy/p/singleton.html