从码农到设计者,从单例模式入手设计代码
首先先复习一下内存
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);
至此,这个单例已经优化完毕。
或许还可以继续优化,但是这个不重要了,讲到这足够了。
我想说的是,你们不要记代码,没用的,试着去理解我的思路,思路是通用的。