『设计模式』—— 单例模式

530 阅读6分钟

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点

核心:确保只有一个实例,并提供全局访问

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建

实现单例模式

要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象
var Singleton = function( name ){
    this.name = name;
    // 实例
    this.instance = null;
};
Singleton.prototype.getName = function(){
    console.log(this.name)
}; 
Singleton.getInstance = function( name ){
    // 如果存在实例,则直接跳过
    if ( !this.instance ){
        this.instance = new Singleton( name );
    }
    return this.instance;
}; 
var a = Singleton.getInstance( 'sven1' );
var b = Singleton.getInstance( 'sven2' );

console.log( a === b ); // true
a.getName(); // sven1
b.getName(); // sven1

或者

var Singleton = function( name ){
    this.name = name;
};
Singleton.prototype.getName = function(){
    console.log(this.name)
};
// 使用闭包来保存实例对象
Singleton.getInstance = (function(){
    var instance = null;
    return function( name ){
        if ( !instance ){
       	    instance = new Singleton( name );
        }
        return instance;
   }
})();

console.log( a === b ); // true
a.getName(); // sven1
b.getName(); // sven1

我们通过 Singleton.getInstance 来获取 Singleton 类的唯一对象,这种方式存在一个问题,就是增加了这个类的“不透明性”,Singleton 类的使用者必须知道这是一个单例类, 必须使用 Singleton.getInstance 来获取对象,不能通过以往new来获取对象

用代理实现单例模式

// 构造函数
var CreateSingleton = function( name ){
    this.name = name;
    this.init(); 
}
CreateSingleton.prototype.init = function(){
    // 这里执行初始化方法
    console.log( this.name )
} 
/**
 * 引入代理类-proxyClass来负责管理单例
 * 这里使用了自执行的匿名函数和闭包,将变量封装在闭包的内部
 */
var proxyClass = (function(){
    var instance; 
    return function( name ){
         // 如果存在实例,则跳过
         if ( !instance ){
         	instance = new CreateSingleton( name );
         } 
         return instance;
    }
})(); 

var a = new proxyClass('singleton1'); 
var b = new proxyClass('singleton2');
console.log( a === b ); // true

通过引入代理,我们可以直接通过new来获取单例对象,并且把负责管理单例的逻辑转移到了代理类-proxyClass中,这样一来CreateSingleton就变成了一个普通的类,它跟proxyClass组合起来可以达到单例模式的效果

JavaScript 中的单例模式

前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从 “类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在 Java 中,如果需要某个 对象,就必须先定义一个类,对象总是从类中创建而来的

但JavaScript 其实是一门无类语言,在 JavaScript 中创建对象的方法非常简单,直接创建一个变量对象,反正我们只需要一个“唯一”的对象,没必要为它先创建一个“类”呢?这无异于穿棉衣洗澡

单例模式的核心是确保只有一个实例,并提供全局访问

var a = {};

之后我们就有点迷惑了,既然如此,那我们直接创建一个唯一的变量就是了,但是全局变量存在很多问题,它很容易造成命名空间污染,所以我们要用几种方式来降低全局变量带来的命名污染

① 使用命名空间

适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量
var namespace = {
    a: function(){
       alert (1);
    },
    b: function(){
       alert (2);
    }
}; 

② 使用闭包封装私有变量

这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信
// 我们用下划线来约定私有变量__name 和__age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染
var user = (function(){
    var __name = 'sven',
        __age = 29;
    return {
        getUserInfo: function(){
            return __name + '-' + __age;
        }
    }
})(); 

惰性单例

惰性单例指的是在需要的时候才创建对象实例

实际上在开头的代码中头就使用过这种技术, instance 实例对象总是在我们调用 Singleton.getInstance 的时候才被创建,而不是在页面加载好 的时候就创建,代码如下:

Singleton.getInstance = (function(){
    var instance = null;
    return function( name ){
        if ( !instance ){
            instance = new Singleton( name );
        }
        return instance;
   }
})();

不过这是基于“类”的单例模式, JavaScript 中并不适用,接下来展示js中与全局变量结合实现惰性的单例

var getSingle = function(fn){
  var result
  return function(){
    return result || ( result = fn.apply(this, arguments) )
  }
}
var createLoginLayer = function(){
  var div = document.createElement( 'div' )
  div.innerHTML = '我是登录浮窗'
  div.style.display = ' none '
  document.body.appendChild( div )
  return div
}
//预渲染浮窗
var LoginLayer = getSingle( createLoginLayer )
//打开浮窗
LoginLayer().style.display = 'block'
//关闭浮窗
LoginLayer().style.display = 'none'

但是现在都2020年了,都用框架了,已经很少有完全直接操作DOM的时候了,但是设计模式还是可以用的

Vue中使用惰性单例

惰性单例有两大优点:

单例 —— 只被创建一次

惰性 —— 只加载已“缓存好的”

因此在Vue中我们可以这样优化代码:

①v-if主动控制首次不加载,需要时才加载

②v-show首先默认为true,只在v-if为ture之后再控制显示与隐藏

<template>
  <div>
    <el-button @click="isLoad = true">点击开始加载dialog</el-button>
    <!-- element的对话框组件 -->
    <el-dialog
      title="提示"
      :visible.sync="dialogVisible"
      width="30%"
      v-if="isLoad"
    >
      <span>这是一段信息</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false"
          >确 定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>
<script>
export default {
  data() {
    return {
      isLoad:false,
      dialogVisible: false,
    };
  },
};
</script>

这里是通过点击将isLoad变为true才开始加载el-dialog的,el-dialog的隐藏和显示就是用v-show来控制,这种优化适用于一些占很大资源却对于用户不常有的功能,导致加载时间会被无意义地拖长的情况

Vuex中的单例模式

在项目中引入Vuex:
// 引入vuex插件
import Vuex from 'vuex'
// 安装vuex插件
Vue.use(Vuex)

// 创建vuex的实例
var store =  new Vuex.Store({})
// 将store注入到Vue实例中
new Vue({
    el: '#app',
    store
})

通过调用Vue.use()方法,安装 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue实例里去

let Vue // Vue的作用和上面的instance一样
...

export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

上面便是 Vuex 源码中单例模式的实现办法了。通过这种方式,可以保证一个 Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store

参考

JavaScript设计模式与开发实践--曾探

JavaScript惰性单例与vue性能优化应用

JavaScript设计模式——单例模式

若有错误或者对于vue对于单例模式更好的应用请在评论区留言,最后感谢阅读!!!

设计模式是对语言不足的补充

—— Peter Norvig