单例模式详解:从基础到实践
ES6 之前 JavaScript 无原生类,函数身兼二职(普通函数 / 构造函数)的二义性常引发问题
Person.prototype.run = function() {
console.log('running');
}
Person.say = '你好'
function Person() {
this.name = '橘子'
return 'hello'
}
let p = new Person() // {name: '橘子'}
console.log(Person.say);
—— 比如 function Person() { this.name = '橘子'; return 'hello' } ,用 new 调用会生成带 name 的实例,直接调用却返回字符串还污染全局,而 Person.prototype.run 定义实例方法、Person.say 定义静态属性的写法也不够直观;这让学习了其他编程语言的人感觉到很怪异,官方再ES6 打造了类class 作为语法糖,强制用 new 调用,标准化了实例方法与静态方法的定义,解决了二义性问题,而单例模式则在此基础上进一步限制:一个类仅能创建一个实例,还提供全局访问点,像小区唯一的保安,避免多实例导致的资源浪费与状态混乱。
类的出现顶替了构造函数的写法,换一种新的写法(优雅)
类与单例模式基础
JavaScript 里的函数向来是 "身兼数职" 的多面手 —— 既能当普通函数跑腿,又能披上个new关键字变身构造函数造对象。不过自从有了class,对象创建这事儿才算有了正经章法,不用再让函数干兼职了。
看看里这个Person类:
class Person { // 类,相当于对象的"模板"
constructor() {//构造器
this.name = '橘子' // 每个实例都带自己的name
}
run() { // 实例方法,谁实例化谁能用
console.log('running');
}
static say() { // 静态方法,属于类自己的"独门绝技"
console.log('你好');
}
}
let p = new Person()
p.say()//报错,访问不到
Person.say() // 类直接调用静态方法,不用麻烦实例
这里的
Person就像个模具,每次new一下都能造出个新 "人",个个都有自己的name。但单例模式偏要反其道而行之:一个类只能造一个实例,多一个都不行,还得给个全局都能找到它的门牌号。就像小区保安,全小区就一个,谁找他都得去门卫室。
单例模式的实现方式
单例模式:保证一个类,只有一个实例,并提供一个访问它的全局访问点
我们先看看非单例:
class SingleDog {
show() {
console.log('我是一个单例对象');
}
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
console.log(s1 === s2); //输出false,两个对象的引用地址不同,s1和s2不是同一个实例对象
1. 静态方法实现
这种方式就像给类雇了个 "门卫",想创建实例先问门卫:"里面有人了吗?"
class SingleDog {
show() {
console.log('我是一个单例对象');
}
static getInstance() { // 门卫大爷
// 没人就放一个进来
if (!SingleDog.instance) {//第一次SingleDog.instance不存在为undefined,!取反,if走得进去
//第二次SingleDog.instance是一个对象(空对象),if走不进去
SingleDog.instance = new SingleDog()
}
// 有人就直接把里面的人带出来
return SingleDog.instance//第二次返回
}
}
// 测试一下
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
console.log(s1 === s2); // true(果然是同一只狗)
getInstance这个静态方法就是门卫,首次调用时创建实例,之后不管谁来,都只能见到同一个实例。
2. 闭包实现
在此之前我们先介绍一个 "急性子"—— 自执行函数,定义完不等别人喊,自己就先跑起来了:
(function foo() { // 自执行函数,自带"立即执行"属性
console.log('foo');
})()
这货最擅长的就是搭 "密室",把变量藏在里面,外面谁也碰不到。用它来实现单例,相当于给实例上了把锁:
class SingleDog {
show() {
console.log('我是一个单例对象');
}
}
SingleDog.getInstance = (function foo() {//外面这层函数自动调用
// 一加载就偷偷创建好实例,藏在密室里
let instance = new SingleDog()
return function() {//里面这层函数定义在了外面这层函数里面但没有在里面被调用
return instance // 谁来都只给这一个
}
})()
// 测试
const s1 = SingleDog.getInstance()//在这里被调用,形成了闭包
const s2 = SingleDog.getInstance()
console.log(s1 === s2); // true(还是同一只狗)
和静态方法的 "来人再开门" 不同,这种方式是 "提前把人请进门",不管用不用,实例早早就准备好了。
实战:全局弹框实现(modal.html)
网页里的全局弹框就特别适合单例模式 —— 总不能点一次按钮就冒出一个弹框,不然屏幕很快就被弹框 "占领" 了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例弹框</title>
<style>
#modal {
width: 200px;
height: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid #000;
text-align: center;
}
</style>
</head>
<body>
<button id="btn1">open1</button>
<button id="btn2">open2</button>
<button id="close">close</button>
<script>
const Modal = (function () {
let modal = null // 弹框的"身份证",确保只有一个
return function () {
if (!modal) { // 第一次调用,创建弹框
modal = document.createElement('div')
modal.innerHTML = '我是一个全局弹框'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal // 之后调用,直接返回已有的弹框
}
})()
// 绑定事件
document.getElementById('btn1').addEventListener('click', () => {
const modal = new Modal()
modal.style.display = 'block' // 显示弹框
})
document.getElementById('btn2').addEventListener('click', () => {
const modal = new Modal()
modal.style.display = 'block' // 还是同一个弹框
})
document.getElementById('close').addEventListener('click', () => {
const modal = new Modal()
modal.style.display = 'none' // 隐藏弹框
})
</script>
</body>
</html>
不管你点open1还是open2,页面上始终只有一个弹框在工作,既省资源又不添乱,这就是单例模式的智慧。
总结
单例模式就像给类上了个 "独生子女证",核心是保证实例的唯一性。常用的实现套路有两种:
-
静态方法(懒汉式):用的时候再创建,省内存
-
自执行函数+闭包(饿汉式):提前创建好,用的时候直接拿
像全局状态管理、日志工具、数据库连接这些 "只能有一个" 的角色,都适合用单例模式来管理。记住,单例的精髓不是 "只创建一次",而是 "确保永远只有一次"。