11

写在前面

(度过一阵的繁忙期,又可以愉快的开始学习新知识了,一年来技术栈切来切去,却总觉得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 

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

小结

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


安歌
7k 声望5.5k 粉丝

目前就职于Ringcentral厦门,随缘答题, 佛系写文章,欢迎私信探讨.