单例模式:JavaScript 中 "独一无二" 的对象创建术

91 阅读5分钟

单例模式详解:从基础到实践

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 调用,标准化了实例方法与静态方法的定义,解决了二义性问题,而单例模式则在此基础上进一步限制:一个类仅能创建一个实例,还提供全局访问点,像小区唯一的保安,避免多实例导致的资源浪费与状态混乱。

类的出现顶替了构造函数的写法,换一种新的写法(优雅)

ScreenShot_2025-12-03_193357_132.png

类与单例模式基础

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()  // 类直接调用静态方法,不用麻烦实例

ScreenShot_2025-12-03_191404_608.png 这里的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(果然是同一只狗)

ScreenShot_2025-12-03_193802_074.png

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>

ScreenShot_2025-12-03_200200_114.png

不管你点open1还是open2,页面上始终只有一个弹框在工作,既省资源又不添乱,这就是单例模式的智慧。

总结

单例模式就像给类上了个 "独生子女证",核心是保证实例的唯一性。常用的实现套路有两种:

  • 静态方法(懒汉式):用的时候再创建,省内存

  • 自执行函数+闭包(饿汉式):提前创建好,用的时候直接拿

像全局状态管理、日志工具、数据库连接这些 "只能有一个" 的角色,都适合用单例模式来管理。记住,单例的精髓不是 "只创建一次",而是 "确保永远只有一次"。