从码农到设计者,从单例模式入手设计代码

945 阅读6分钟

首先先复习一下内存

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);

至此,这个单例已经优化完毕。 或许还可以继续优化,但是这个不重要了,讲到这足够了。 我想说的是,你们不要记代码,试着去理解我的思路,思路是通用的。