《JavaScript单例模式详解:从原理到实践》

前言

在软件开发中,设计模式是解决特定问题的经典方案。今天我们要探讨的是单例模式(Singleton Pattern),这是一种创建型设计模式,也是JavaScript中最常用的设计模式之一。无论你是前端新手还是资深开发者,理解单例模式都能让你的代码更加优雅高效。

介绍单例模式

单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点。就像公司里的CEO职位,无论多少人问"谁是CEO",得到的都是同一个对象。

举个栗子!

非单例模式:

js 复制代码
//传统模式
class Dog {
  constructor(name, breed, age) {
    this.name = name;
    this.breed = breed;
    this.age = age;
  }
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);

console.log(dog1 === dog2) //false

单例模式:

js 复制代码
//单例模式
class Dog {
  constructor(name, breed, age) {
    if (Dog.instance) {
      return Dog.instance;
    }
    
    this.name = name;
    this.breed = breed;
    this.age = age;
    
    Dog.instance = this;
  }
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);

console.log(dog1 === dog2);  // true,两个变量引用同一个实例

为什么需要单例?

  • 避免重复创建消耗资源的对象
  • 确保全局状态的一致性
  • 提供对唯一实例的受控访问

在JavaScript中,单例模式常用于:

  • 全局状态管理(如Redux store)
  • 缓存系统
  • 日志记录器
  • 浏览器中的window对象本身就是单例的典型例子

如何实现

下面我们通过一个本地存储封装的示例来演示单例模式的实现:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>设计模式之单例模式</title>
</head>
<body>
    <script>
        /*
         * Storage 本地存储类
         * 单例模式实现
         * @func getItem 获取本地的值
         * @func setItem 设置本地存储的值
         */
        class Storage {
            // 构造函数,可以指定命名空间防止键名冲突
            constructor(namespace = 'storage') {
                this.namespace = namespace;
            }

            // 静态方法获取单例实例
            static getInstance(){
                // 检查是否已存在实例
                if (!Storage.instance) {
                    // 不存在则创建新实例
                    Storage.instance = new Storage();
                }
                // 返回已存在的实例
                return Storage.instance;
            }    
            
            // 获取本地存储的值
            getItem(key) {
                return localStorage.getItem(this.namespace + key);
            }   
    
            // 设置本地存储的值
            setItem(key, value) {
                localStorage.setItem(this.namespace + key, value);
            }
        }

        // 测试单例模式
        const storage1 = Storage.getInstance();
        const storage2 = Storage.getInstance();

        // 验证两个变量是否指向同一个实例
        console.log(storage1 === storage2, '!!!');  // 输出: true '!!!'
        
        // 通过一个实例设置值
        storage1.setItem('name','haha');
        
        // 通过另一个实例获取值
        console.log(storage1.getItem('name'));  // 输出: "haha"
        console.log(storage2.getItem('name'));  // 输出: "haha"
    </script>
</body>
</html>

代码解读:

  1. 我们创建了一个Storage类来封装localStorage操作

  2. 关键点在于getInstance静态方法:

    • 它检查类属性Storage.instance是否存在
    • 不存在时才会创建新实例
    • 确保无论调用多少次getInstance()都返回同一个实例
  3. 测试代码验证了storage1storage2确实是同一个实例

  4. 并且通过一个实例修改的值,可以从另一个实例正确获取

用闭包实现单例

这里再提一种用闭包实现单例的方法,直接看代码

js 复制代码
// 1. 基础构造函数
function StorageBase() {
    // 空构造函数
}

// 2. 在原型上添加方法
StorageBase.prototype.getItem = function(key) {
    return localStorage.getItem(key);
}

StorageBase.prototype.setItem = function(key, value) {  // 注意:参数列表中缺少value
    return localStorage.setItem(key, value);  // 修正:原代码是settItem拼写错误
}

// 3. 单例封装(核心部分)
const Storage = (function() {
    let instance = null;  // 闭包保存单例实例
    
    return function() {   // 返回真正的构造函数
        if(!instance) {
            instance = new StorageBase();  // 首次调用时创建实例
        }
        return instance;  // 总是返回同一个实例
    }
})();  // 立即执行

// 4. 测试
const storage1 = Storage();
const storage2 = Storage();
console.log(storage1 === storage2)  // true

这是一种es6之前,没有class语法糖的写法: 使用了闭包+立即执行函数的方式来实现单例模式,是一种经典的实现方式。下面我们逐部分解析:

  1. 闭包保存单例实例

    • 使用IIFE(立即执行函数)创建闭包环境
    • instance变量被闭包保护,外部无法直接访问
  2. 惰性初始化(Lazy Initialization)

    • 只有第一次调用Storage()时才会创建实例
    • 后续调用直接返回已创建的实例
  3. 原型方法

    • 将方法挂在StorageBase.prototype
    • 所有实例共享这些方法(虽然这里只有一个实例)

与之前的class方式实现相比,两种方式的区别:

特性 类实现 闭包函数实现
实例存储位置 类的静态属性 闭包变量
实现方式 class语法 构造函数+原型链
访问方式 静态方法getInstance() 直接函数调用
现代性 ES6+ ES5
私有性 较弱 闭包提供更好的封装

单例模式的重要性

单例模式在前端开发里可太重要啦,为啥这么说呢,因为它能解决下面这些问题:

  1. 资源优化:像数据库连接、WebSocket 连接这些老占资源的对象,用它就能避免反复创建
  2. 状态一致性:能保证像用户信息这种全局状态,在整个应用里都保持一致
  3. 内存管理:能减少那些没必要的内存占用,特别是在像移动端这种资源不咋够的环境里
  4. 访问控制:弄个统一的访问入口,以后要是想加日志、做验证啥的逻辑,都方便

实际应用场景:

  • 全局状态管理库(如Redux、Vuex的store)
  • 模态框/对话框管理
  • 缓存系统
  • 日志记录器
  • 浏览器中的window/document对象

注意事项:

  • 单例模式可能引入全局状态,过度使用会导致代码难以测试
  • 在JavaScript中要考虑线程安全(虽然JS是单线程的)
  • 考虑使用依赖注入来改善可测试性

通过合理使用单例模式,我们可以构建出更加健壮、高效的JavaScript应用。

相关推荐
谢尔登33 分钟前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js
蓝婷儿1 小时前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我1 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
OpenTiny社区1 小时前
告别代码焦虑,单元测试让你代码自信力一路飙升!
前端·github
kk_stoper1 小时前
如何通过API查询实时能源期货价格
java·开发语言·javascript·数据结构·python·能源
pe7er1 小时前
HTTPS:本地开发绕不开的设置指南
前端
晨枫阳2 小时前
前端VUE项目-day1
前端·javascript·vue.js
江山如画,佳人北望2 小时前
SLAM 前端
前端
患得患失9492 小时前
【前端】【Iconify图标库】【vben3】createIconifyIcon 实现图标组件的自动封装
前端
颜酱2 小时前
抽离ant-design后台的公共查询设置
前端·javascript·ant design