js设计模式(一)-单例模式

8

写在前面

(度过一阵的繁忙期,又可以愉快的开始学习新知识了,一年来技术栈切来切去,却总觉得js都还没学完-_-)

本文主要围绕js的设计模式进行展开,对每个设计模式从特征,原理和实现方式几个方面进行说明。由于内容较长,所以拆分成多篇文章。如果有不对的地方欢迎指出,阅读前请注意几点:

  1. 如果对js的类式继承和闭包不太熟练的建议先阅读相关内容,比如我前面写过的js继承(主要看到原型链继承部分就好)js闭包,,
  2. 知识密度较大,建议边思考,顺便跑以下相关的代码(如果碰到代码有问题的欢迎指出),中途注意休息

正文

定义

也叫单体模式,核心思想是确保一个类只对应一个实例
虽然js是弱类型的语言,但是js也有构造函数和实例。所以这里可以理解为确保多次构造函数时,都返回同一个实例

实现

根据定义,我们需要实现一个构造函数,并且满足以下条件:

function A(){
    //需要实现的函数内容
}
var a1 = new A() 
var b1 = new A()
a1 ==== b1 //true

在前面我们说到了构造函数和实例,并且也知道了引用类型的值赋值的时候存放的实际是变量的地址指针,所以要实现这个构造函数的核心思路是:每次调用构造函数时,返回指向同一个对象的指针。 也就是说,我们只在第一次调用构造函数时创建新对象,之后调用返回时返回该对象即可。所以重点变成了--如何缓存初次创建的变量对象。

首先先排除全局变量,因为一般情况下需要保证全局环境的纯净,其次全局变量容易被改写,出现意外情况。所以采用以下2种方案来实现缓存。

1. 使用构造函数的静态属性

因为构造函数本身也是对象,可以拥有静态属性。所以可以这样实现:

function A(name){
    // 如果已存在对应的实例
   if(typeof A.instance === 'object'){
       return A.instance
   }
   //否则正常创建实例
   this.name = name
   
   // 缓存
   A.instance =this
   return this
}
var a1 = new A() 
var a2= new A()
console.log(a1 === a2)//true

这种方法的缺点在于静态属性是能够被人为重写的,不过不会像全局变量那样被无意修改。

2. 借助闭包

通过闭包的方式来实现的核心思路是,当对象第一次被创建以后,重写构造函数,在重写后的构造函数里面访问私有变量。

function A(name){
  var instance = this
  this.name = name
  //重写构造函数
  A = function (){
      return instance
  }
}
var a1 = new A() 
var a2= new A()
console.log(a1 === a2)//true

到这里我们其实已经实现了最核心的步骤,但是这样的实现存在问题,如果看过原型链继承的小伙伴会注意到,如果我们在第一次调用构造函数之后,由于构造函数被重写,那么在之后添加属性和方法到A的原型上,就会丢失。比如:

function A(name){
  var instance = this
  this.name = name
  //重写构造函数
  A = function (){
      return instance
  }
}
A.prototype.pro1 = "from protptype1"

var a1 = new A() 
A.prototype.pro2 = "from protptype2"
var a2= new A()

console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//underfined
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//underfined

重写构造函数之后,,实际上原先的A指针对应的函数实际上还在内存中(因为instance变量还在被引用着,这里的内容如果忘记了请看闭包),但是此时A指针已经指向了一个新的函数了,可以简单测试下:

console.log(a1.constructor ==== A)//false

所以接下来我们应该解决这个问题,根据上文可知,我们的重点是,调整原型实例之间的关系,所以应该这样实现(这一块忘记的还是建议回头看看js继承里面的那张函数、原型、实例之间的关系图点击直达):

function A(name){
  var instance = this
  this.name = name
 
  //重写构造函数
  A = function (){
      return instance
  }
  
  // 第一种写法,这里实际上实现了一次原型链继承,如果不想这样实现,也可以直接指向原来的原型
  A.prototype = this
  // 第二种写法,直接指向旧的原型
  A.prototype = this.constructor.prototype
  
  instance = new A()
  
  // 调整构造函数指针,这里实际上实现了一次原型链继承,如果不想这样实现,也可以直接指向原来的原型
  instance.constructor = A
  
  return instance
}
A.prototype.pro1 = "from protptype1"

var a1 = new A() 
A.prototype.pro2 = "from protptype2"
var a2= new A()

console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2

现在一切就正常了。还有一种方式,是利用立即执行函数来保持私有变量,(立即执行函数的内容请看《详解js中的函数部分》)原理也是闭包:

var A;
(function(name){
    var instance;
    A = function(name){
        if(instance){
            return instance
        }
        
        //赋值给私有变量
        instance = this
        
        //自身属性
        this.name = name
    }
}());
A.prototype.pro1 = "from protptype1"

var a1 = new A('a1') 
A.prototype.pro2 = "from protptype2"
var a2 = new A('a2')

console.log(a1.name)
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2

简单说明一下上面的内容,首先利用在立即执行函数中保存一个私有变量instance,初次执行之后,第一次调用new A()之后,生成一个对象并让instance指向该对象,从第二次开始,调用new A(),都只返回这个对象,

*特殊情况

很多地方会提到,使用字面量直接创建一个对象也是一个单例模式的实例。这个说法我个人觉得并不够严格,和同事探讨之后觉得可能是这样(如果有有其他见解的小伙伴欢迎指出):使用字面量写法的时候,实际上相当于使用原生的Object函数new了一个对象,然后存储到内存里,之后我们每次使用对应的指针去读取时,读到的都是这个对象。而我不认为这是一个单例模式的原因如下:

var obj1 = new Object({
  name:111
})

var obj2 = new Object({
  name:111
})

console.log(obj1===obj2)//false 

我觉得既然两次调用同一个构造函数,返回的不是同一个对象,那不就不能成为单例模式。当然,这一部分是我个人的看法,读者朋友还是要注意区分。

小结

单例模式先说到这里,后面会陆续补充其他的设计模式。
感谢之前的热心读者,尤其是为我指出错误的小伙伴
然后依然是每次都一样的结尾,如果内容有错误的地方欢迎指出;如果对你有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页添加了邮箱地址~溜了


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

小石头若海 · 2018年06月25日

且不说特殊情况的观点对不对,但是作者举的例子是不是就自相矛盾了?

原文:很多地方会提到,使用字面量直接创建一个对象也是一个单例模式的实例。

这里说的是字面量创建的对象,但是举的例子是用new的方式创建了obj1和obj2。

回复

0

首先感谢你的评论,我的意思是,直接使用字面量,实际上可能只是一种语法糖,在js内部执行的还是new方式创建对象。所以举例的时候举的是new的方式创建对象的例子,文中可能描述的不够准确~

安歌 作者 · 2018年06月25日
0

@安歌 那你可以修改Object的构造函数 比如加个log 然后看看用字面量形式创建对象的时候 有没有这个log就知道了

小石头若海 · 2018年06月25日
0

@小石头若海 这个思路是有一些问题的:重写Object构造函数之后,字面量表示法实际上依然可以重走旧的构造函数,原理参照文中的第二种方式;字面量表示法完全可以是初始Object函数的另一种实现,只要初始的Object函数还在内存中,这个方式就依然依然有效,而内置对象的方法在浏览器初始化的时候就生成了,没有办法直接修改;正是由于这样的情况并且还没找到确切的文档依据的情况下, 我才使用了new方式来尝试理解。如果你找到了相关的文档或者想到了可以实现的方式,很期待你的分享~

安歌 作者 · 2018年06月26日
陆山青 · 5月16日

你最后一个例子两个obj不相等不是当然的么,和new 没有关系啊 你新建一个obj 会开一个堆内存空间 假如地址分别是aaafff111 和aaafff222 两个内存地址不一样,当然不可能相等

字面量单例模式不是你这么用的啊
var singleton = {

    attr : 1,
    method : function(){ return this.attr; }
}

var t1 = singleton ;
var t2 = singleton ;
那么很显然的, t1 === t2 。 这样只开辟一个堆内存空间 t1=aaafff111 t2=aaafff111

单例模式的定义不是 把描述同一个事物的属性和特征进行“分组,归类”(存储在同一个堆内存空间中)
你开两个内存空间,就不是单例模式了

回复

陆山青 · 5月17日

还有js和后端编程语言不一样,你最后举的例子是js构造函数模式,不是单例模式 ,后端单例模式是必须new一个类才可以的,js是允许字面量的

回复

载入中...