前端设计模式:单例模式(Singleton)


00、基本概念

单例模式(Singleton Pattern),也称单体模式,就是全局(或某一作用域范围)唯一实例,大家共享、复用一个实例对象,也可减少内存开销。单例模式应该是最基础、也最常见的设计模式了。

✅常见场景

  • 全局状态vuex,Jquery中的全局对象$,浏览器中的window、document 都算是单例。
  • 公共的服务、全局配置、缓存、登录框等,全局复用一个对象。

所以实现单例模式的关键就是保障对象实例只创建一次,后续的引用都是同一个实例对象。相比于Java、C#等语言,JavaScript单线程,也没有类,单例实现还是比较容易,基于JS语言特性,有多种实现思路。

实现方式 说明
全局对象 全局环境下的var变量,或者直接挂载到全局对象window上。使用简单,但会存在全局污染,也不优雅,🚫不推荐!
构造函数.静态方法getInstance 使用构造函数的静态方法getInstance()来获取实例,唯一实例对象存储在构造函数的instance上。
虽有一定耦合,Class版本还是一种不错的方式
闭包-new 利用JS的闭包(万恶的闭包)来保存那个唯一对象实例,这样就可以new来获取唯一实例对象了
ES6模块Module ES6的模块其实就是单例模式,模块中导出的对象就是单例的,多次导入其实是同一个引用。

01、全局对象(不推荐)

创建一个全局对象,浏览器中全局对象一般挂载在Window上,如JQuery、loadsh就是如此实现的。

  • 在全局环境中用 var 字面量申明一个对象,利用了var的变量提升 + 全局属性的特点(全局环境下的var变量会自动成为全局属性),所以慎用var
  • 直接挂载到全局对象window上。
javascript 复制代码
window.jQuery = window.$ = jQuery;
window._ = lodash;

// 全局环境下的var变量会自动成为全局属性
var singleUser = {
  name: 'sam',
  id: 1001
}
// 使用
console.log(singleUser.name)  // sam
console.log(window.singleUser.name)  // sam

02、构造函数.静态方法getInstance

统一一个入口获取对象实例,入口就是为构造函数的静态方法getInstance()(当然命名随意),在该函数中判断(静态)对象instance是否初始化,没有则创建,有则直接返回。所以实际上的唯一实例是作为静态属性,保存在构造器的instance属性上,类似Math.PI

javascript 复制代码
function GlobalUser(name) {
  this.name = name
  this.id = 1002
}
// 基于构造函数的静态函数作为统一入口,Constructor.getInstance()
GlobalUser.getInstance = function(name) {
  // 注意这里的this指向的是构造函数GlobalUser
  if (this.instance) return this.instance
  // 第一次没有创建
  return this.instance = new GlobalUser(name)
}
console.log(GlobalUser.getInstance('张三').name)   // 张三
console.log(GlobalUser.getInstance('李四').name)   // 张三,依然是张三,复用了第一次创建的实例
console.log(GlobalUser.getInstance() === GlobalUser.getInstance())  // true

ES6的Class 版本的,原理和上面一样,因为Class本质上也是基于原型的构造函数,但实现起来更优雅一些,推荐使用。

javascript 复制代码
class GlobalUser {
  constructor(name) {
    this.name = name
    this.id = 1002
  }
  static getInstance(name) {
    //静态方法属于类本身,这里的this也就指向类本身
    if (!this.instance)
      this.instance = new GlobalUser(name)
    return this.instance;
  }
}
console.log(GlobalUser.getInstance('张三').name)   // 张三
console.log(GlobalUser.getInstance('李四').name)   // 张三,依然是张三,复用了第一次创建的实例
console.log(GlobalUser.getInstance() === GlobalUser.getInstance())  // true

03、闭包-new

核心思路就是利用JS的闭包(万恶的闭包)来保存那个唯一对象实例,这样就可以new来获取唯一实例对象了!基于闭包的实现方式是比较多的,下面示例只是其中一种,但基本原理都是利用闭包来保存那个"唯一实例"。

javascript 复制代码
let GlobalUser = (function() {
  let instance  // 闭包保存的唯一实例对象
  return function(name) {
    if (instance) return instance
    // (首次)创建实例
    instance = { name: '张三', id: 1003 }
    return instance
  }
})()  // 立即执行,外层函数的价值就是他的闭包变量instance
console.log(new GlobalUser('张三').name)   // 张三
console.log(new GlobalUser('李四').name)   // 张三,依然是张三,复用了第一次创建的实例 
console.log(new GlobalUser() === new GlobalUser())  // true

断点输出一下日志可以看到GlobalUser的构造函数闭包

闭包版本还可以继续改进下,做成一个通用版本的单例工厂:把具体的对象示例构造器封装一下。

javascript 复制代码
// 一个通用单例工厂,参数为构造器函数、Class类
let Singleton = function(Constructor) {
  let instance
  return function(...args) {
    if (instance) return instance
    // (首次)创建实例
    instance = new Constructor(...args)
    return instance
  }
}

// 构造函数
function User(name) {
  this.name = name
}
class Config {
  constructor(title) {
    this.title = title
  }
}
// 使用
let SingleUser = Singleton(User)
let u1 = new SingleUser('sam')
let u2 = new SingleUser('zhangsan')
console.log(u1 === u2, u1, u2)  //true User {name: 'sam'} User {name: 'sam'}

let GlobalConfig = Singleton(Config)
console.log(new GlobalConfig('设计') === new GlobalConfig('模式'))  // true

04、ES6模块Module

ES6的模块其实就是单例模式,模块中导出的对象就是单例的,多次导入其实是同一个引用。

回顾一下ESM:参考《ESModule模块化

  • 📢 Singleton 模式:import模块的代码只会执行一次,同一个url文件只会第一次导入时执行代码。后续任何地方import都不会执行模块代码了,也就是说,import语句是 Singleton 模式的。
  • 📢 只读-共享 :模块导入的接口的是只读的,不能修改。当然引用对象的属性值是可以修改的,不建议这么干,注意模块是共享的,导出的是一个引用,修改后其他方也会生效。

因此用ESM实现单例就比较简单了:

javascript 复制代码
// 模块申明 config.js
export default {
  title: '设计模式'
}

// 使用
import config from './config.js'
console.log(config)  // {title: '设计模式'}
config.title = '修改一下'

import config2 from './config.js'
console.log(config, config2)  // {title: '修改一下'} {title: '修改一下'}

参考资料


©️版权申明 :版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀

相关推荐
HUMHSX36 分钟前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货1 小时前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙0071 小时前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由1 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317421 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
谢尔登2 小时前
【React】 状态管理方案
前端·react.js·前端框架
用户2136610035722 小时前
Vue商品详情与放大镜组件
前端·javascript
半个落月2 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
李明卫杭州2 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js
李明卫杭州2 小时前
使用 computed 处理 v-model 复杂数据结构
前端·javascript·vue.js