前端设计模式:单例模式(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: '修改一下'}

参考资料


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

相关推荐
花花鱼5 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09338 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang135830 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning30 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人40 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css
嚣张农民2 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
周亚鑫2 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf