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

参考资料


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

相关推荐
编程猪猪侠21 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞25 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架