首先先複習一下記憶體
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.constructor
和 myDiv1.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);
複製程式碼
至此,這個單例已經優化完畢。
或許還可以繼續優化,但是這個不重要了,講到這足夠了。
我想說的是,你們不要記程式碼,試著去理解我的思路,思路是通用的。