JavaScript单例模式详解:从ES5闭包到Redux实战
本文深入解析JavaScript中的单例模式,阐述其保证全局唯一实例的核心思想。通过ES5闭包与ES6类静态属性两种方式实现单例,并对比提前渲染与按需创建的优劣。进一步抽象出通用单例生成器,结合Redux单一数据源等特性,展示该模式在大型前端架构中的实际价值与内存优化效果。
单例模式
若追求代码的优雅性、高可维护性及简洁度,设计模式往往是不可或缺的解决方案。
在JavaScript的设计模式体系中,最核心的理念在于封装变化:将易变部分与稳定部分分离,确保变化部分具备灵活性,而稳定部分保持恒定。
接下来介绍第一种常见的设计模式:单例模式。
单例模式旨在确保一个类仅存在一个实例,并提供一个全局访问点。其目的是解决全局使用的类因频繁创建和销毁而导致的内存占用问题。
ES5中通过闭包
在ES5环境下,可以利用闭包来保存类的唯一实例。闭包是指函数内部返回的函数被外部变量引用,导致函数内部的变量无法被释放,从而形成闭包结构。
var Singeton = (function(){
var instance;
function User(name,age){
this.name=name;
this.age=age;
}
return function(name,age){
if(!instance){
instance = new User(name,age)
}
return instance
}
})()此时,一旦该实例被生成,后续每次调用均返回此实例且不会被修改。如图所示,当给User对象初始赋值name为alice、age为18时,后续再赋值将无效,每次返回的均为初始实例对象。

ES6中使用类的静态属性
上述代码采用ES6语法实现,通过类的静态属性来保存唯一的实例对象。
class Singeton {
constructor(name,age){
if(!Singeton.instance){
this.name = name;
this.age = age;
Singeton.instance = this;
}
return Singeton.instance;
}
}创建方式依然相同,通过new关键字创建类的实例对象。
案例
这种设计模式在开发中究竟有何实际用途?试想这样一个业务场景:用户访问网站时,若长时间未操作页面导致授权过期,此时点击页面上任意位置,都会弹出一个登录框。
该登录框必须是全局唯一的,不存在多份副本,也不会相互冲突。因此无需每次创建新实例,保留初始的那一份即可。
提前创建节点
我们可能会想到首先在页面中提前创建节点,编写好页面样式,最后通过控制元素的display属性来达到显示和隐藏的效果。
<div class="modal">登录对话框</div>
<button id="open">打开</button>
<button id="close">关闭</button>
<style>
.modal {
display: none;
/* 其他布局代码省略 */
}
</style>
<script>
document.querySelector("#open").onclick = function(){
const modal = document.querySelector('.modal')
modal.style.display = 'block'
}
</script>这样可以完成需求,全局只有一个登录框,每次都展示同一个。但问题是dom元素从一开始它创建好并添加到body中,无论是否需要用到,如果有些场景不需要登陆,那么这里初始渲染就会浪费空间。
单例模式
那如果不需要初始渲染,仅当需要时才使用,并且每次都返回同一个实例的单例模式应该如何实现呢?
我们可以这样处理
<!-- 去除class为modal的标签,动态创建 -->
<script>
const Modal = (function(){
let instance = null
return function(){
if(!instance){
instance = document.createElement("div")
instance.innerHTML = "登录对话框"
instance.className = "modal"
instance.style.display = "none"
document.body.appendChild(instance)
}
return instance
}
})()
document.querySelector("#open").onclick = function(){
//创建modal,如果放在外面,一开始就会创建元素
const modal = Modal()
//显示modal
modal.style.display = "block"
}
document.querySelector("#close").onclick = function(){
const modal = Modal()
modal.style.display = "none"
}
</script>虽然上面的方式可以达到效果,但是创建对象和管理单例的逻辑都放在了对象内部,是有些混乱的。并且如果下次需要创建页面中唯一的 iframe,或者 script 标签,就得将以上函数照抄一遍。
通用单例
首先拆分函数逻辑,将执行创建对象的逻辑拿出来
const createLayer = function(){
let div = document.createElement("div")
div.innerHTML = "登录对话框"
div.className = "modal"
div.style.display = "none"
document.body.appendChild(div);
return div;
}
const Modal = (function(){
let instance = null
return function(){
if(!instance){
instance = createLayer()
}
return instance
}
})()以上修改后代码逻辑就更为清晰,但此时还不支持通用化的创建别的组件,这时候我们想想如何将创建单例的方法进行一些优化,是否可以将单例需要执行的函数抽象化。
const createSingle = (function(fn){
let instance;
return function(){
return instance || ( instance = fn.apply(this, arguments))
}
})()这样改造后,如果存在创建 iframe 的方法,也可以直接使用。
const createIframe = function() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
return iframe
}
const singleIframe = createSingle(createIframe)
document.querySelector("#open").onclick = function(){
const iframe = singleIframe()
iframe.style.display = 'block'
}实际应用
以上都是咱小打小闹的试用,那再来看看社区中一些非常棒的实现吧~ 比如:React 中常用的状态管理工具 Redux 就使用到了单例模式,它有这样一些要求。
- 单一数据源:整个应用的 state 只存在于唯一一个 store 中。
- State 是只读的:不要直接改变 state 的值,唯一改变 state 的方法就是触发 action。
- reducer 是纯函数:需要编写纯函数 reducer 来修改 state 的值。
来看看 Redux 的源码,为了便于阅读已删减部分逻辑判断和注释,可以看到通过 store 的 getState 方法每次获取闭包中的 currentState。

单例模式在内存中只有一个实例,可以减少内存开支,同时还能在系统设置全局的访问点,优化和共享资源。
以上就是单例模式的相关介绍。更多有关 前端、设计模式 的内容请关注本站其它相关文章!
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
