JavaScript 中如何优雅地实现单例?多种方案对比解析

JavaScript 中如何优雅地实现单例?多种方案对比解析

单例模式(Singleton Pattern) 是一种常用的创建型设计模式,在 JavaScript 中,由于其动态语言特性和灵活的对象模型,实现单例的方式也多种多样。

一、 什么是单例模式?

单例模式的核心思想是:保证一个类只有一个实例,并提供一个全局访问点来获取这个实例。

这种模式常用于管理全局状态配置对象日志记录器数据库连接等需要唯一实例的场景。这是因为这些对象创建和销毁的成本比较高,使用单例可以避免重复创建,减少开销。

二、JavaScript 中的单例实现方式

  • 使用对象字面量(Object Literal)

    这是最简单、最直接的一种方式,适用于不需要延迟加载的场景。

    js 复制代码
    const Singleton = {
      value: 42,
      getValue() {
        return this.value;
      }
    };
    
    console.log(Singleton.getValue()); // 输出 42

    这种方式的代码简洁,易于理解和维护,但不具备延迟初始化能力,不能动态创建实例。

  • 使用闭包 + 工厂函数 + IIFE

    通过闭包封装私有变量,实现真正的惰性加载(Lazy Initialization)。

    js 复制代码
    // 构造函数
    function StorageBase() {}
    StorageBase.prototype.get = function (key) {
      return localStorage.getItem(key);
    };
    StorageBase.prototype.set = function (key, value) {
      localStorage.setItem(key, value);
    };
    
    // 闭包+立即执行函数  Storage是闭包实际上是闭包函数,这个闭包函数中引用了instance这个自由变量
    const Storage = (function () {
        // instance 用于指向StorageBase的实例对象
      let instance = null;
      return function () {
        if (!instance) {
          instance = new StorageBase();
        }
        return instance;
      };
    })();
    const storage = new Storage();
    const storage2 = new Storage();
    console.log(storage === storage2); // true

    代码解析:

    • 在上述这段代码中StorageBase是一个普通的构造函数,提供get与set,底层使用的是localStorage来实现存储。
    • Storage是一个立即执行函数返回的函数 ,也就是闭包函数。在这里利用闭包维护一个私有变量instance,这个instance用于存储StorageBase的实例化对象。当Storage第一次执行时,会创建StorageBase的实例化对象并存储到instance这个自由变量中,之后每次执行Storage这个构造函数都会返回这个实例化对象。
    • 使用new关键字创建一个实例对象时,若构造函数返回的是一个对象或函数,那么new表达式将不会再返回之前构造函数创建的新对象而是直接返回这个指定的对象。上述构造函数Storage返回的便是一个对象,所以会有上述的结果。

    使用闭包 + 工厂函数 + IIFE的优点是实现了惰性加载,保证线程安全(在 JavaScript 单线程环境中),但这种方式略复杂,需要理解闭包和立即执行函数。

  • 使用 ES6 类 + 静态方法

    JavaScript 是一门基于原型的语言,早期并没有 class 的概念,而是通过函数和原型链来实现面向对象编程。ES6 引入了 class 语法糖,使得代码更易读、结构更清晰,但其底层依然是基于原型的。

    js 复制代码
    class Storage {
        // 静态方法用于获取实例
        static getInstance() {
            // 静态方法中的 this 指向类本身
            if (!this.instance) {
                this.instance = new Storage();
            }
            return this.instance;
        }
    
        // 构造函数
        constructor() {
            // 构造函数中的 this 指向实例对象
            this.storage = window.localStorage;
        }
    }
    
    const storage1 = Storage.getInstance();
    const storage2 = Storage.getInstance();
    
    console.log(storage1 === storage2); // true

    在面向对象中publicprivate等关键字都是属性实例对象上的,而static是属于类的。静态方法只能访问静态属性和静态方法,实例方法既可以访问实例属性、方法,也可以访问静态属性、方法。

    代码解析:

    • getInstance() 是一个静态方法,调用时 this 指向的是类本身。

    • 第一次调用时,this.instance 不存在,这时会在类上动态添加一个静态属性instance 。之后创建Storage的实例(走第三步)并赋值给它。后续调用会直接返回 this.instance,从而保证单例。

    • constructor是一个构造方法,调用时this指向的是实例对象 ,this.storage = window.localStorage 实际上是在实例对象上面动态添加了storage属性这个属性值,这个属性值是window.localStorage

    使用这种方式如果直接使用 new Singleton(),可能会绕过单例逻辑,需要始终使用 getInstance() 方法。

  • 使用 ES6 模块导出(Module Pattern)

在 ES6 模块系统中,模块本身是天然的单例,非常适合用来实现全局共享对象。

js 复制代码
// singleton.js
export default {
  value: 42,
  getValue() {
    return this.value;
  }
};

其他模块中使用:

js 复制代码
import singleton from './singleton.js';
console.log(singleton.getValue()); // 输出 42

优点 :简洁、天然单例,适合现代前端项目。 缺点:不支持惰性加载,除非手动封装。

  • 使用 Proxy 实现更灵活控制(进阶)

    通过 Proxy 拦截构造函数调用,可以更灵活地控制实例的创建过程。

    js 复制代码
    let instance = null;
    
    const SingletonProxy = new Proxy({}, {
      construct() {
        if (!instance) {
          instance = { value: 42 };
        }
        return instance;
      }
    });
    
    const a = new SingletonProxy();
    const b = new SingletonProxy();
    
    console.log(a === b); // true

    优点 :灵活,可以拦截和控制对象创建行为。 缺点:理解成本高,适用于进阶用途。

三、各种 实现方式对比表

实现方式 是否惰性加载 是否推荐 适用场景
对象字面量 快速定义常量对象
闭包封装 惰性加载、传统写法
类 + 静态方法 面向对象风格、逻辑封装
模块导出 ES6+ 模块系统、共享配置对象
Proxy ⚠️ 高级控制、特殊需求
相关推荐
brzhang14 分钟前
OpenAI 7周发布Codex,我们的数据库迁移为何要花一年?
前端·后端·架构
军军君0132 分钟前
基于Springboot+UniApp+Ai实现模拟面试小工具三:后端项目基础框架搭建上
前端·vue.js·spring boot·面试·elementui·微信小程序·uni-app
布丁052332 分钟前
DOM编程实例(不重要,可忽略)
前端·javascript·html
bigyoung34 分钟前
babel 自定义plugin中,如何判断一个ast中是否是jsx文件
前端·javascript·babel
指尖的记忆1 小时前
当代前端人的 “生存技能树”:从切图仔到全栈侠的魔幻升级
前端·程序员
草履虫建模1 小时前
Ajax原理、用法与经典代码实例
java·前端·javascript·ajax·intellij-idea
轻语呢喃1 小时前
useReducer : hook 中的响应式状态管理
javascript·后端·react.js
时寒的笔记1 小时前
js入门01
开发语言·前端·javascript
陈随易1 小时前
MoonBit能给前端开发带来什么好处和实际案例演示
前端·后端·程序员
996幸存者1 小时前
uniapp图片上传组件封装,支持添加、压缩、上传(同时上传、顺序上传)、预览、删除
前端