导读:本期聚焦于半糖创作的《如何在JavaScript中实现单例模式?五种常用写法详解与适用场景分析》,敬请观看详情。单例模式是前端面试的高频考点,也是实际项目中管理全局状态的常用手段。本文围绕JavaScript实现单例模式的几种典型方式展开,包括闭包实现、ES6 Class结合静态方法、Proxy拦截、模块化天然单例以及惰性单例的写法,分析了每种方案的原理、优缺点和适用场景。文末还对比了不同实现方式的差异,帮助你根据业务需求选择合适的方案,比如弹窗组件、全局缓存、配置对象等场景该如何落地。

单例模式的核心目标很简单:保证一个类在整个应用程序中只有一个实例,并提供一个全局访问点。在JavaScript里,这个需求其实非常常见——全局的登录弹窗、统一的请求缓存、全局配置对象、音视频播放器等,都希望只存在一份实例,避免重复创建带来的资源浪费和状态混乱。JavaScript没有传统面向对象语言的private构造函数,所以实现单例的方式相当灵活,下面详细介绍几种主流写法。

如何在JavaScript中实现单例模式?五种常用写法详解与适用场景分析

闭包实现单例:最经典的写法

闭包是实现单例最直接的手段。思路是把实例缓存在闭包变量里,每次调用都判断这个变量是否已经有值,有就直接返回,没有才创建。来看一个基础版本:

function createLoginLayer() {
  var instance = null;
  return function () {
    if (!instance) {
      // 创建登录弹窗 DOM
      instance = document.createElement('div');
      instance.innerHTML = '我是登录弹窗';
      instance.style.display = 'none';
      document.body.appendChild(instance);
    }
    return instance;
  };
}
var getLoginLayer = createLoginLayer();
var a = getLoginLayer();
var b = getLoginLayer();
console.log(a === b); // true

这段代码的关键在于instance变量被外层函数的作用域保护起来,外部无法直接访问和修改,只能通过返回的函数获取。两次调用返回的是同一个DOM节点,比较结果为true,说明单例生效了。

这种写法的优点是兼容性好,任何支持闭包的环境都能跑,而且把创建逻辑和单例管理逻辑分离得比较清楚。缺点是代码结构上稍显松散,如果需要给多个不同的类都加单例能力,就得重复写类似的判断逻辑,违反了开放封闭原则。

代理模式优化:把单例逻辑和业务逻辑解耦

上面的写法把“创建弹窗”和“保证唯一”两件事揉在一个函数里,更好的做法是用代理模式把二者分开。单例逻辑放在代理函数中,业务函数只负责创建对象,这样任何创建函数都能复用单例能力:

// 普通的创建函数,只关心怎么创建
function createLoginLayer() {
  var div = document.createElement('div');
  div.innerHTML = '我是登录弹窗';
  div.style.display = 'none';
  document.body.appendChild(div);
  return div;
}

// 代理函数,负责管理单例
var proxyCreateLoginLayer = (function () {
  var instance = null;
  return function () {
    if (!instance) {
      instance = createLoginLayer();
    }
    return instance;
  };
})();

这种结构的好处非常明显:将来如果要给购物车弹窗、消息提示框做单例,只需要换掉被代理的创建函数,代理逻辑完全不用动。这是《JavaScript设计模式与开发实践》一书中的经典案例,也是面试中经常被追问的进阶写法。

需要注意的是,代理模式要求被代理的函数本身不持有状态,如果创建函数依赖外部变量或者内部维护了自己的缓存,代理层的控制可能会被绕过,这一点在重构老代码时要格外小心。

ES6 Class结合静态方法:现代主流方案

ES6之后,用Class配合静态方法实现单例成为更常见的写法。静态属性属于类本身而不是实例,天然适合存放那个唯一的实例:

class Singleton {
  constructor() {
    if (Singleton.instance) {
      throw new Error('请使用 Singleton.getInstance() 获取实例');
    }
    this.data = {};
    Singleton.instance = this;
  }

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true
new Singleton(); // 抛出错误,阻止外部直接 new

这个版本不仅提供了全局访问点,还在构造函数里做了防御,外部直接new会直接报错,强制使用者走getInstance入口。相比闭包写法,Class版本的语义更清晰,代码组织也更符合现代工程习惯。

如果追求更简洁的写法,还可以利用静态字段直接简化:

class Config {
  static instance = null;
  constructor(options) {
    if (Config.instance) return Config.instance;
    this.options = options;
    Config.instance = this;
  }
}

这个版本的构造函数会在第二次被调用时直接返回已存在的实例,外部甚至感知不到区别,new Config()多少次拿到的都是同一个对象。缺点是失去了一种显式的获取方式,代码可读性上略逊于getInstance方案。

模块化与Proxy:更工程化的选择

很多开发者忽略了一点:ES Module本身就是天然的单例。模块在被首次导入时执行一次,之后无论多少个文件导入它,拿到的都是同一个模块实例。所以最简单的单例可能根本不需要写任何模式代码:

// store.js
export const globalStore = {
  user: null,
  token: '',
  setUser(user) {
    this.user = user;
  }
};

// 任意多个文件中导入,globalStore 都是同一个对象
import { globalStore } from './store.js';

这种方式在Vue、React项目中极为常见,全局状态、工具方法集合、配置对象都可以这样处理。它的好处是零成本、无侵入,缺点是模块加载时就创建了对象,如果这个对象初始化开销很大却未必会被用到,就浪费了资源。

针对这个问题,可以引入惰性单例(Lazy Singleton)的思想:把实例化推迟到第一次真正使用的时候。结合Proxy可以写出比较优雅的实现:

const lazySingleton = (className) => {
  return new Proxy({}, {
    get(target, prop) {
      // 第一次访问属性时才真正实例化
      if (!target.instance) {
        target.instance = new className();
      }
      const value = target.instance[prop];
      return typeof value === 'function'
        ? value.bind(target.instance)
        : value;
    }
  });
};

class HeavyService {
  constructor() {
    console.log('昂贵的初始化,只在首次访问时执行');
    this.data = new Array(1000000).fill(0);
  }
  getData() { return this.data.length; }
}

const service = lazySingleton(HeavyService);
console.log('尚未实例化');
console.log(service.getData()); // 此时才打印初始化日志

Proxy版本的优势在于使用者完全不需要关心单例细节,直接像操作普通对象一样使用即可,实例化时机被完全隐藏在拦截层。不过要注意get拦截器中函数绑定this的处理,否则方法调用时上下文会丢失。

各方案对比与选型建议

综合来看,几种方案各有定位。闭包写法适合快速实现和面试演示;代理模式适合需要复用单例逻辑的多对象场景;ES6 Class静态方法适合作为类库的标准接口暴露;ES Module导出适合项目内的全局状态管理,是工程中最省事的方式;惰性单例则适合初始化成本高的对象,比如大型数据结构、WebSocket连接池等。

选型时可以问自己两个问题:这个对象是否初始化开销大且可能用不到?是否需要在多个不同类之间复用单例能力?前者指向惰性单例,后者指向代理模式,两者都不是的话,直接用模块导出往往就够了。另外提醒一点,单例模式本质上引入了全局状态,在单测中可能造成状态污染,测试时最好提供重置实例的手段,比如增加一个静态的resetInstance方法,避免用例之间互相影响。

JavaScript单例模式闭包ES6 Class修改时间:2026-09-11 09:24:36

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0911/54584.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。