2 创建型-单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点 ,这样的模式就叫做单例模式。

2.1 单例模式的实现思路

  • 单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例
  • 要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例 的能力。我们现在把这段判断逻辑写成一个静态方法(其实也可以直接写入构造函数的函数体里):
    // 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
    class Singleton {
        constructor(name) {
            this.name = name;
            this.instance = null;
        }
    
        getName(){
            console.log(this.name);
        }
    
        getInstance(name){
            if(!this.instance){
                this.instance = new Singleton(name);
            }
    
            return this.instance;
        }
    }
    
    const singleton = new Singleton();
    
    const a = singleton.getInstance('a');
    const b = singleton.getInstance('b');
    
    console.log(a);
    console.log(b);
    console.log(a === b);
    class SingleDog {
        show() {
            console.log('我是一个单例对象')
        }
        static getInstance() {
            // 判断是否已经new过1个实例
            if (!SingleDog.instance) {
                // 若这个唯一的实例不存在,那么先创建它
                SingleDog.instance = new SingleDog()
            }
            // 如果这个唯一的实例已经存在,则直接返回
            return SingleDog.instance
        }
    }
    
    const s1 = SingleDog.getInstance()
    const s2 = SingleDog.getInstance()
    
    // true
    s1 === s2

除了楼上这种实现方式之外,getInstance的逻辑还可以用闭包 来实现:

    SingleDog.getInstance = (function() {
        // 定义自由变量instance,模拟私有变量
        let instance = null
        return function() {
            // 判断自由变量是否为null
            if(!instance) {
                // 如果为null则new出唯一实例
                instance = new SingleDog()
            }
            return instance
        }
    })()

可以看出,在getInstance方法的判断和拦截下,我们不管调用多少次,SingleDog都只会给我们返回一个实例,s1s2现在都指向这个唯一的实例

2.2 生产实践:Vuex中的单例模式

ReduxVuex,它们都实现了一个全局的 Store 用于存储应用的所有状态。这个 Store 的实现,正是单例模式的典型应用

1. 理解 Vuex 中的 Store

Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 ——Vuex官方文档

当组件非常多、组件间关系复杂、且嵌套层级很深的时候,这种原始的通信方式会使我们的逻辑变得复杂难以维护。这时最好的做法是将共享的数据抽出来、放在全局,供组件们按照一定的的规则去存取数据,保证状态以一种可预测的方式发生变化。于是便有了 Vuex,这个用来存放共享数据的唯一数据源,就是 Store。

2. Vuex如何确保Store的唯一性

我们先来看看如何在项目中引入 Vuex

    // 安装vuex插件
    Vue.use(Vuex)
    
    // 将store注入到Vue实例中
    new Vue({
        el: '#app',
        store
    })

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

install方法里,有一段逻辑和我们楼上的 getInstance 非常相似的逻辑:

    let Vue // 这个Vue的作用和楼上的instance作用一样
    ...
    
    export function install (_Vue) {
      // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的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 源码中单例模式的实现办法了,套路可以说和我们的getInstance如出一辙。通过这种方式,可以保证一个 Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store

3. 思考时间

思考一下:如果我在 install 里没有实现单例模式,会带来什么样的麻烦?

我们通过上面的源码解析可以看出,每次 install 都会为Vue实例初始化一个 Store。假如 install 里没有单例模式的逻辑,那我们如果在一个应用里不小心多次安装了插件:

    // 在主文件里安装Vuex
    Vue.use(Vuex)
    
    ...(中间添加/修改了一些store的数据)
    
    // 在后续的逻辑里不小心又安装了一次
    Vue.use(Vuex)

失去了单例判断能力的 install 方法,会为当前的Vue实例重新注入一个新的 Store,也就是说你中间的那些数据操作全都没了,一切归 0。因此,单例模式在此处是非常必要的。

除了说在 Vuex 中大展身手,我们在 ReduxjQuery 等许多优秀的前端库里也都能看到单例模式的身影

2.3 单例模式——面试真题

实现一个 Storage

1. 描述

实现Storage,使得该对象为单例,基于 localStorage 进行封装。实现方法 setItem(key,value)getItem(key)

2. 思路

具体实现上,把判断逻辑写入静态方法或者构造函数里都没关系,最好能把闭包的版本也写出来

3. 实现:静态方法版

    // 定义Storage
    class Storage {
        static getInstance() {
            // 判断是否已经new过1个实例
            if (!Storage.instance) {
                // 若这个唯一的实例不存在,那么先创建它
                Storage.instance = new Storage()
            }
            // 如果这个唯一的实例已经存在,则直接返回
            return Storage.instance
        }
        getItem (key) {
            return localStorage.getItem(key)
        }
        setItem (key, value) {
            return localStorage.setItem(key, value)
        }
    }
    
    const storage1 = Storage.getInstance()
    const storage2 = Storage.getInstance()
    
    storage1.setItem('name', '李雷')
    // 李雷
    storage1.getItem('name')
    // 也是李雷
    storage2.getItem('name')
    
    // 返回true
    storage1 === storage2

4. 实现: 闭包版

    // 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
    function StorageBase () {}
    StorageBase.prototype.getItem = function (key){
        return localStorage.getItem(key)
    }
    StorageBase.prototype.setItem = function (key, value) {
        return localStorage.setItem(key, value)
    }
    
    // 以闭包的形式创建一个引用自由变量的构造函数
    const Storage = (function(){
        let instance = null
        return function(){
            // 判断自由变量是否为null
            if(!instance) {
                // 如果为null则new出唯一实例
                instance = new StorageBase()
            }
            return instance
        }
    })()
    
    // 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果 
    const storage1 = new Storage()
    const storage2 = new Storage()
    
    storage1.setItem('name', '李雷')
    // 李雷
    storage1.getItem('name')
    // 也是李雷
    storage2.getItem('name')
    
    // 返回true
    storage1 === storage2

2.4 实现一个全局的模态框

实现一个全局唯一的Modal弹框

思路

这道题比较经典,基本上所有讲单例模式的文章都会以此为例,同时它也是早期单例模式在前端领域的最集中体现。

万变不离其踪,记住getInstance方法、记住instance变量、记住闭包和静态方法,这个题除了要多写点 HTML 和 CSS 之外,对大家来说完全不成问题。

实现

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>单例模式弹框</title>
    </head>
    <style>
        #modal {
            height: 200px;
            width: 200px;
            line-height: 200px;
            position: fixed;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            border: 1px solid black;
            text-align: center;
        }
    </style>
    <body>
    	<button id='open'>打开弹框</button>
    	<button id='close'>关闭弹框</button>
    </body>
    <script>
        // 核心逻辑,这里采用了闭包思路来实现单例模式
        const Modal = (function() {
        	let modal = null
        	return function() {
                if(!modal) {
                	modal = document.createElement('div')
                	modal.innerHTML = '我是一个全局唯一的Modal'
                	modal.id = 'modal'
                	modal.style.display = 'none'
                	document.body.appendChild(modal)
                }
                return modal
        	}
        })()
        
        // 点击打开按钮展示模态框
        document.getElementById('open').addEventListener('click', function() {
            // 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
        	const modal = new Modal()
        	modal.style.display = 'block'
        })
        
        // 点击关闭按钮隐藏模态框
        document.getElementById('close').addEventListener('click', function() {
        	const modal = new Modal()
        	if(modal) {
        	    modal.style.display = 'none'
        	}
        })
    </script>
    </html>

是不是发现又是熟悉的套路?又可以默写了?(ES6 版本的实现大家自己尝试默写一下,相信对现在的你来说已经非常简单了)。

这就是单例模式面试题的特点,准确地说,是所有设计模式相关面试题的特点——牢记核心思路,就能举一反三。所以说设计模式的学习是典型的一分耕耘一分收获,性价比极高。

Last Updated:
Contributors: leeguooooo