所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

3分钟搞懂!JavaScript单例模式怎么写+用在哪,前端面试工作都能用

3分钟搞懂!JavaScript单例模式怎么写+用在哪,前端面试工作都能用 一

文章目录CloseOpen

JavaScript单例模式怎么写?3种常用方法+实操细节

单例模式的实现思路其实就一句话:用“封装”把实例藏起来,只暴露一个“获取实例”的入口。但在JavaScript里,不同场景有不同的实现方式,我 了3种最常用的——闭包经典版、ES6模块化版、懒加载类版,每个都给你讲清楚“怎么写”“为什么这么写”,以及我踩过的坑。

  • 闭包封装:兼容性拉满的“传统玩法”
  • 闭包是JavaScript里的“老工具”了,用来实现单例特别适合——用外层函数保存实例,内层函数负责创建/返回实例,因为闭包会保留外层函数的作用域,实例不会被垃圾回收。

    我去年帮同事改一个电商项目的“加入购物车提示弹框”时,就用了这个方法。他之前的代码是“点击一次创建一个弹框”,结果用户点5次,页面上飘5个弹框。我帮他改成闭包单例后,不管点多少次,都只有一个弹框实例:

    // 封装弹框的单例函数
    

    function createCartToast() {

    let instance; // 保存实例的变量,存在外层函数作用域里

    return function(content) {

    if (!instance) { // 第一次调用时,创建实例

    instance = document.createElement('div');

    instance.className = 'cart-toast';

    instance.style.cssText = 'position: fixed; bottom: 50px; left: 50%; transform: translateX(-50%); padding: 10px 20px; background: #333; color: #fff; border-radius: 4px;';

    document.body.appendChild(instance);

    }

    // 不管有没有实例,都更新内容并显示

    instance.textContent = content;

    instance.style.display = 'block';

    // 3秒后隐藏

    setTimeout(() => {

    instance.style.display = 'none';

    }, 3000);

    return instance;

    }

    }

    // 获取弹框的入口函数

    const showCartToast = createCartToast();

    // 调用:点一次显示一次,但只有一个实例

    showCartToast('已加入购物车');

    showCartToast('库存不足');

    这里的关键是外层函数createCartToast里的instance变量——它被内层函数“记住”了,就算外层函数执行完,instance也不会被销毁。每次调用showCartToast,都会先检查instance是否存在:没有就创建,有就直接用。

    我当时改完,同事拍着大腿说:“原来闭包还能这么用!之前我一直以为闭包只能用来做私有变量,没想到单例也靠它。”不过要注意:闭包单例要手动管理实例——如果需要“重置实例”(比如用户退出登录后清空弹框内容),得加个reset方法,手动把instance设为null,不然下次调用还是旧实例。

  • ES6模块化:最简洁的“现代方案”
  • 如果你的项目用了ES6模块化(比如Vue/React项目),实现单例更简单——模块本身就是单例的!因为ES6模块的加载是“静态的”,一旦模块被加载,导出的内容就固定了,不管导入多少次,都是同一个对象。

    我现在做React项目时,常用这种方法写全局工具类。比如写一个api.js,封装Axios请求:

    // api.js(ES6模块)
    

    import axios from 'axios';

    // 创建Axios实例(单例)

    const apiInstance = axios.create({

    baseURL: 'https://api.example.com',

    timeout: 5000

    });

    // 添加请求拦截器(全局只执行一次)

    apiInstance.interceptors.request.use(config => {

    const token = localStorage.getItem('token');

    if (token) {

    config.headers.Authorization = Bearer ${token};

    }

    return config;

    });

    // 导出单例实例

    export default apiInstance;

    然后在组件里导入:

    // Home.js
    

    import api from './api.js';

    // 不管导入多少次,api都是同一个Axios实例

    async function fetchData() {

    const res = await api.get('/home');

    console.log(res.data);

    }

    这种方法的优点是简洁到“不用额外写逻辑”——模块帮你管好了实例。但要注意:模块是“按需加载”的,如果你的项目用了代码分割(Code Splitting),要确保模块只被加载一次,不然可能会创建多个实例(不过一般项目里不会有这问题)。

  • 懒加载类:面向对象的“优雅写法”
  • 如果喜欢用“类”来组织代码,可以用“懒加载”的方式实现单例——在类的构造函数里检查实例,不存在就创建,存在就返回已有实例

    我之前写一个“全局状态管理器”时,用了这种方法:

    class GlobalState {
    

    constructor() {

    // 关键:检查有没有已存在的实例

    if (GlobalState.instance) {

    return GlobalState.instance; // 直接返回已有实例

    }

    // 第一次创建时,初始化状态

    this.state = {

    userInfo: null,

    theme: 'light'

    };

    // 保存实例到类的静态属性里

    GlobalState.instance = this;

    }

    // 修改状态的方法

    setState(key, value) {

    this.state[key] = value;

    // 触发状态更新(比如通知组件重新渲染)

    console.log('状态更新:', this.state);

    }

    }

    // 使用:不管new多少次,都是同一个实例

    const state1 = new GlobalState();

    const state2 = new GlobalState();

    console.log(state1 === state2); // true

    state1.setState('theme', 'dark');

    console.log(state2.state.theme); // dark(共享状态)

    这种写法的好处是符合面向对象的思维,但要注意:如果子类继承这个类,可能会破坏单例——比如子类的构造函数没检查父类的实例,就会创建新的实例。所以如果用类实现单例,最好在父类里加一个“禁止继承”的逻辑(比如用Object.freeze冻结类)。

    3种方法对比:选对工具做对事

    为了帮你快速选到适合的方法,我整理了一个对比表格——都是我实际项目里用过的经验,直接拿去用:

    实现方式 优点 缺点 适用场景
    闭包封装 兼容性好(支持IE8+);手动控制实例 需要写额外逻辑;易忘“重置实例” 旧项目、需要兼容低版本浏览器
    ES6模块化 简洁;自动管理实例;支持Tree Shaking 依赖ES6模块系统;不适合旧项目 现代前端项目(Vue/React/Angular)
    懒加载类 面向对象;逻辑清晰 继承时易破坏单例;代码稍繁琐 需要用类组织代码的场景(如状态管理器)

    单例模式到底用在哪?4个前端高频场景+避坑技巧

    写会了单例模式,你可能会问:“我知道怎么写,但什么时候该用?”其实前端项目里,只要需要“唯一实例”的场景,都能用到单例。我 了4个最常用的场景,每个都附带我踩过的坑——帮你少走弯路。

  • 弹框/提示组件:避免重复创建DOM
  • 弹框是单例模式的“经典用武之地”——不管是提示弹框、确认弹框还是加载弹框,都应该只有一个实例。我之前做一个教育类项目时,写了个“课程报名确认弹框”,一开始没用到单例,结果用户点“确认报名”3次,页面上出现3个弹框,用户直接懵了:“我到底报名成功没?”

    后来改成单例后,代码是这样的(用ES6模块化):

    // ConfirmModal.js
    

    import React from 'react';

    import ReactDOM from 'react-dom/client';

    // 单例弹框组件

    class ConfirmModal extends React.Component {

    render() {

    const { visible, content, onConfirm } = this.props;

    if (!visible) return null;

    return (

    {content}

    );

    }

    }

    // 保存弹框的根节点和实例

    let modalRoot;

    let modalInstance;

    // 显示弹框的方法(单例入口)

    export function showConfirmModal(options) {

    if (!modalRoot) {

    modalRoot = document.createElement('div');

    document.body.appendChild(modalRoot);

    }

    if (!modalInstance) {

    modalInstance = ReactDOM.createRoot(modalRoot);

    }

    // 更新弹框的props

    modalInstance.render(

    visible={true}

    {...options}

    onCancel={() => modalInstance.render()}

    />

    );

    }

    现在不管调用多少次showConfirmModal,都只有一个弹框实例——既减少了DOM操作,又避免了用户 confusion。避坑提醒:弹框的“隐藏”逻辑要写对——不要直接删除DOM,而是把visible设为false,这样下次显示时不用重新创建实例,性能更好。

  • 全局状态管理:Vuex/Redux的“底层逻辑”
  • 你用Vuex或Redux时,有没有想过:“为什么整个应用只能有一个Store?”答案就是单例模式!因为全局状态需要“唯一数据源”——如果有多个Store,状态就会分裂,组件之间无法同步数据。

    尤雨溪在Vue文档里明确说过:“Vuex的Store是单例的,因为我们需要一个全局的状态容器来管理应用的所有状态。”我之前做Vue项目时,用Vuex管理用户的“收藏课程”列表,不管在“课程详情页”还是“个人中心页”,调用this.$store.commit('addFavorite', courseId),都是操作同一个Store实例,数据同步得特别丝滑。

    避坑提醒:不要试图创建多个Store——我之前见过一个新手,为了“分开管理不同模块的状态”,创建了两个Store,结果“收藏课程”的状态在“个人中心”不显示,查了3小时才发现是Store多实例的问题。记住:Vuex/Redux的Store必须是单例,模块划分用modules就够了。

  • 工具类/服务:避免重复初始化
  • 像Axios请求拦截器、时间格式化工具、缓存服务这些“全局工具”,都应该用单例——初始化一次就够了,重复初始化会浪费内存。

    我之前做一个新闻类项目时,写了个“本地缓存服务”,用来存用户的“浏览记录”:

    // CacheService.js(ES6模块化单例)
    

    class CacheService {

    constructor() {

    this.storage = localStorage;

    }

    // 存数据

    set(key, value, expire = 86400) { // 默认过期时间1天

    const data = {

    value: value,

    expire: Date.now() + expire * 1000

    };

    this.storage.setItem(key, JSON.stringify(data));

    }

    // 取数据

    get(key) {

    const data = JSON.parse(this.storage.getItem(key));

    if (!data) return null;

    if (Date.now() > data.expire) {

    this.remove(key);

    return null;

    }

    return data.value;

    }

    // 删除数据

    remove(key) {

    this.storage.removeItem(key);

    }

    }

    // 导出单例实例

    export default new CacheService();

    然后在组件里导入:

    // NewsList.js
    

    import CacheService from './CacheService.js';

    // 存浏览记录

    function saveBrowseHistory(newsId) {

    const history = CacheService.get('browseHistory') || [];

    history.push(newsId);

    CacheService.set('browseHistory', history);

    }

    这个服务不管导入多少次,都是同一个实例——初始化时只连接一次localStorage,不会重复创建。避坑提醒:工具类的“初始化逻辑”要放在构造函数里,不要放在原型方法里——比如我之前把this.storage = localStorage写在set方法里,结果每次调用set都重新赋值,虽然不影响功能,但没必要。

  • 配置对象:统一管理全局配置
  • 项目里的“全局配置”(比如API地址、主题颜色、版本号),也适合用单例——避免散落各地的“魔法值”。我现在做项目时,都会写一个config.js,用单例存所有配置:

    // config.js(ES6模块化单例)
    

    const config = {

    apiBaseUrl: process.env.NODE_ENV === 'production' ? 'https://api.example.com' 'http://localhost:3000',

    themeColor: '#2f54eb',

    appVersion: 'v1.2.3'

    };

    // 冻结配置对象,防止被修改

    Object.freeze(config);

    export default config;

    这样不管在哪个组件里,都能拿到统一的配置——比如import config from './config.js',然后用config.apiBaseUrl发请求。避坑提醒:用Object.freeze冻结配置对象,防止有人不小心修改配置(比如把themeColor改成红色),导致整个项目风格乱掉。

    其实单例模式的本质,就是“用封装解决重复”——把“创建实例”的逻辑藏起来,只给用户一个“获取实例”的入口。我做前端这么多年,见过很多新手把单例模式想得太复杂,其实只要记住:能少创建一个实例,就少创建一个——毕竟内存是有限的,用户的耐心也是有限的。

    如果你按这些方法试了,欢迎回来告诉我效果!或者你还有什么单例模式的问题,评论区一起聊~


    本文常见问题(FAQ)

    单例模式的核心到底是什么?

    单例模式的核心其实特简单,就是保证一个类或者对象在整个项目里只能有一个实例。怎么做到呢?靠“封装”——把创建实例的逻辑藏起来,只给外面留一个“获取实例”的入口,这样不管谁用,拿到的都是同一个东西。比如购物车弹框,不管点多少次,都只有一个弹框实例,不会飘出来好几个占内存。

    闭包实现单例要注意什么?

    用闭包实现单例得注意俩点:一是得记得手动管理实例,比如要是需要重置(比如用户退出登录后清空弹框内容),得自己写个方法把保存实例的变量设为null;二是闭包会保留外层函数的作用域,别不小心把无关变量也包进去,不然可能占内存。我之前帮同事改购物车弹框时,就差点忘了加重置逻辑,后来用户反馈退出后再进弹框还是旧内容,才赶紧补上。

    ES6模块化为什么能直接实现单例?

    ES6模块本身就是单例的!因为模块加载是静态的,一旦加载过,导出的内容就固定了,不管你在多少个组件里导入,拿到的都是同一个东西。比如你写个api.js封装Axios实例,导出后不管在Home.js还是Detail.js里导入,用的都是同一个Axios实例,不用额外写逻辑,特省心。不过得注意,要是项目用了代码分割,得确保模块只加载一次,不然可能出问题,但一般项目里不会有这情况。

    弹框组件为什么一定要用单例?

    弹框不用单例的话,用户点多少次就会创建多少个DOM节点,既占内存又影响体验。比如我之前做教育项目的报名确认弹框,一开始没用到单例,用户点3次就出3个弹框,用户都懵了,还问“我到底报名成功没?”改成单例后,不管点多少次,都只有一个弹框实例——要显示就更新内容,要隐藏就设visible为false,既省内存又不会让用户 confusion。

    全局配置用单例有什么好处?

    全局配置用单例能避免“魔法值”散落各地。比如你把API地址、主题颜色、版本号这些配置存到一个config.js里,用Object.freeze冻住,导出后不管在哪个组件里用,拿到的都是统一的配置。要是不用单例,可能有人在组件里直接写“https://api.example.com”,后来要改生产环境的地址,得挨个文件找,特麻烦。用单例的话,改config.js里的一处就行,还能防止有人不小心修改配置——毕竟冻住了,想改也改不了。

    原文链接:https://www.mayiym.com/49616.html,转载请注明出处。
    0
    显示验证码
    没有账号?注册  忘记密码?

    社交账号快速登录

    微信扫一扫关注
    如已关注,请回复“登录”二字获取验证码