從碼農到設計者,從單例模式入手設計程式碼

樑音發表於2018-12-24

首先先複習一下記憶體

var str1 = "abc";
var str2 = "abc";
console.log(str1 == str2)
console.log(str1 === str2)
// 上面的程式碼實際上是執行了這個操作
// var str = String("abc")
複製程式碼
// 那麼如果
var str1 = String("abc");
var str2 = new String("abc");
console.log(str1 == str2)
console.log(str1 === str2)
// 我們可以在控制檯輸出一下 str1 和 str2,一看就知道為什麼不一樣了。
複製程式碼

我們可以得到結論:

  • == 比較的是變數(物件)的值
  • === 比較的是變數(物件)的地址
  • 以後不管什麼程式語言,只要看到 new 關鍵字,一定是在堆中開闢一塊新記憶體

瀏覽器解析 HTML

模板和例項

<div id="myDiv"></div>
複製程式碼
var myDiv = document.getElementById("myDiv");
複製程式碼
  • 瀏覽器把 HTML 一對對標籤解析下來後,全部存放到記憶體空間,並互相指向,形成所謂的 DOM 樹
  • 可以用typeof mydiv的方式檢視
  • 如果是 Object 那就一定是存放在堆空間的,其他存放在常量池。
  • 這個 myDiv 實際上是 div 標籤的例項
  • 可以用 myDiv.constructor 的方法看這個例項到底是哪個例項類

那麼能不能用 new HTMLDivElement() new 一個新的 HTMLDiv 類呢?

不管對錯,先猜猜看。

實際操作一遍發現瀏覽器報錯了,瀏覽器不允許你私自 new 一個 HTMLDiv 類
那麼該如何 new 出一個 HTMLDiv 類呢?

————————————–我是分割線————————————–

瀏覽器提供了這麼一個方法 document.createElement("div"),通過這種方式,它能夠在記憶體中建立一個 DOM 物件,並且是 HTMLDivElement 的物件。

var myDiv1 = document.createElement("div");
複製程式碼

這是一個典型的工廠模式。
我們可以發現 myDiv.constructormyDiv1.constructor 一模一樣,這就說明,這兩個是同樣一個模板下所產生的不同的例項

但是問題來了,我希望我的模板下有且只有一個例項,節約記憶體,例如 body 標籤,全域性唯一,這個時候該怎麼設計?

————————————–我是引導君————————————–

JavaScript 有一個特性,就是動態物件可以隨意複製其行為和屬性。

大家來說出自己的理解。

不要著急,我先繼續往下講。

var obj = {};
// 這就是一個簡單的單例,同時也是 Object 的一個例項,我們可以在這裡面擴充套件任何我們想要的屬性
// 例如 var Obj = {name: "abc",age: 1}
複製程式碼

這段程式碼在我們專案程式碼裡非常普遍,但是這樣的例項,也是單例模式,有個不好的地方,那就是,這個單例根本無法擴充套件,而且使用起來也非常不安全,因為我們可以隨時改變這個裡面的內容。

————————————–我是不正經的正題君————————————–

那麼,單例模式在 JavaScript 中該如何設計呢?

(function(){})()
// 熟悉我的寫法的人肯定知道
// 這段程式碼是建立一個匿名函式,並且立即呼叫
// 那麼這麼寫到底有什麼用呢?
複製程式碼

這種寫法是有用的,它幫我實現了一個閉包,這段程式碼的 {} 中幫我實現了一個閉包臨時作用域。

var SingleTest = (function(){
    // 這個 return 的 function 就是剛剛說的模板類
    return function(){
        console.log("進入構造器函式");
    }
})()

// 這個時候我們就可以
var i1 = new SingleTest();
var i2 = new SingleTest();
console.log(i1===i2) // false
複製程式碼

大家注意,有基礎的應該都知道,函式在 JavaScript 裡面有兩種使用方式,一種是函式呼叫(小寫),另一種是構造器(大寫),行業潛規則。

但是我希望不管我怎麼 new ,我都想使用記憶體中的同一塊地址,也就是i1===i2,那麼該怎麼做呢?

var SingleTest = (function(){
    var _instance = null;
    return function(){
        console.log("進入構造器函式");
        if (!_instance) {
            console.log("第一次 new,區域性變數(例項)_instance 為 null");
            _instance = this;
            return _instance;
        } else {
            console.log("不是第一次 new,區域性變數(例項)_instance 不為 null,直接返回");
            return _instance;
        }
    }
})()
複製程式碼

大家先理解理解這段程式碼。

this 代表的是當前建立的這塊記憶體空間的引用,所以定義的屬性或者方法都可以用 this 來操作。

這個時候 console.log(i1===i2) 看看會發生什麼。

————————————–我是不正經的引數君————————————–

如果我要給這個單例傳一個引數,我們要訪問例項裡面的 name 屬性怎麼辦?

var SingleTest = (function(){
    var _instance = null;
    return function(ops){
        // 我們經常會這麼做
        // 通過這種方式我們可以過濾掉不傳引數帶來的空引用問題
        ops = ops || {};
        if (!_instance) {
            _instance = this;
            // 通過 for 迴圈,遍歷迭代我們的引數
            for (var prop in ops) {
                _instance[prop] = ops[prop];
            }
            return _instance;
        } else {
            for (var prop in ops) {
                _instance[prop] = ops[prop];
            }
            return _instance;
        }
    }
})()

var i1 = new SingleTest({name:"zhangsan"});
var i2 = new SingleTest({name:"lisi"});
console.log(i1.name);
// 那麼輸出的值是多少呢?
// 很明顯上面寫了兩個 for 迴圈,我們程式碼裡也經常有這種情況發生,這個時候該怎麼優化?
複製程式碼
var SingleTest = (function(){
    var _instance = null;
    var _default = {}
    // 封裝 for 迴圈
    function _init(ops) {
        for (var prop in ops) {
            this[prop] = ops[prop];
        }
    }
    return function(ops=_default){// es6支援
        // ops = ops || {}; 這種方式已經 out 了
        if (!_instance) {
            _instance = this;
            _init.call(_instance, ops);
        } else {
            _init.call(_instance, ops);
        }
        return _instance;
    }
})()

var i1 = new SingleTest({name:"zhangsan"});
var i2 = new SingleTest({name:"lisi"});
console.log(i1.name);
複製程式碼

這個程式碼已經優化度很高了,但是還可以進行優化,比如說,如果我這裡面不止 _init 方法,還有其他方法,例如,function _method1(){} function _method2(){}等,在函式體內進行呼叫的時候,你會發現,這麼設計並不是一個好主意。
那麼,有多個方法的時候該如何進行優化呢?

————————————–我是正經的優化君————————————–

簡單來說,就是將這些方法加到原型鏈中。

var SingleTest = (function(){
    var _instance = null;
    var _default = {}
    function SingleInstance(ops=_default){
        if (!_instance) {
            _instance = this;
            this._init(ops);
        } else {
            _instance._init(ops);
        }
        return _instance;
    }
    // 將方法加到原型中去
    // _的意思是,私有屬性或方法,行業規則。
    SingleInstance.prototype._init = function(ops) {
        for (var prop in ops) {
            this[prop] = ops[prop];
        }
    }
    return SingleInstance;
})()

var i1 = new SingleTest({name:"zhangsan"});
var i2 = new SingleTest({name:"lisi"});
console.log(i1.name);
複製程式碼

你會發現,這樣做程式碼量並沒有減少多少,但是優點是,在寫入其他方法的時候,可以用 this 直接相互呼叫已存在的方法。
還有一個優點是,如果後期想 new 出不同的例項,直接對 _instance 做處理就好了,因為我已經把這個單例打包成了閉包,不會影響外面的呼叫者。

但是這樣還有一個 bug !
那就是如果有些人不上規矩,想直接呼叫 SingleTest({name:"mazi"}) ,這個時候你會發現,控制檯報錯了。
那麼該如何優化,讓這種呼叫也相容呢?

————————————–我是萬惡的bug君————————————–

原因就是,這樣做是直接呼叫這個函式堆疊,這就意味著,當前函式的作用域並不是堆裡面的 this
不要問我函式堆疊是什麼,這個不是主題,簡單來說就是把函式拿到棧裡面去執行。

奔主題。

var SingleTest = (function(){
    var _instance = null;
    var _default = {}
    function SingleInstance(ops=_default){
        // instanceof 是表示 this 是不是 SingleInstance 的例項
        if (this instanceof SingleInstance) {
            if (!_instance) {
                _instance = this;
                this._init(ops);
            } else {
                _instance._init(ops);
            }
        } else {
            if (!_instance) {
                _instance = new SingleInstance();
                _instance._init(ops);
            } else {
                _instance._init(ops);
            }
        }
        return _instance;
    }
    SingleInstance.prototype._init = function(ops) {
        for (var prop in ops) {
            this[prop] = ops[prop];
        }
    }
    return SingleInstance;
})()

var i0 = SingleTest({name:"wangwu"})
var i1 = new SingleTest({name:"zhangsan"});
var i2 = new SingleTest({name:"lisi"});
console.log(i0 === i1);
console.log(i0 === i2);
複製程式碼

至此,這個單例已經優化完畢。
或許還可以繼續優化,但是這個不重要了,講到這足夠了。
我想說的是,你們不要記程式碼,試著去理解我的思路,思路是通用的。

相關文章