面试必考:手把手教你实现单例模式与 LocalStorage 封装

100 阅读6分钟

面试必考:掌握单例模式,高效封装 LocalStorage

在前端开发中,设计模式是构建可维护、可扩展代码的基石。单例模式(Singleton Pattern),它确保一个类只拥有一个实例,并提供一个全局访问点。本文将深入剖析单例模式的原理,并结合实际场景,教你如何优雅地封装 localStorage,实现一个高效的单例存储对象。


为什么需要单例模式?

想象一下,你的应用中有多个地方需要访问同一个配置对象、日志记录器或数据缓存。如果每次都创建新的实例,不仅会造成资源浪费,还可能导致状态不一致。单例模式正是为了解决这些问题而生,它的主要价值体现在:

  • 性能优化: 避免重复创建对象的开销,尤其对于资源密集型操作(如数据库连接、大型配置加载)。
  • 资源统一管理: 确保所有模块都操作同一个实例,便于协调和控制共享资源。
  • 避免全局变量污染: 将相关功能封装在一个单一实例中,减少对全局命名空间的侵扰。
  • 状态一致性: 保证整个应用程序对某个特定资源的访问是同步和一致的。

JavaScript 中实现单例模式的两种主流方式

JavaScript 提供了灵活的方式来实现单例模式,最常见且实用的有两种:基于 ES6 class 的静态方法和基于闭包。

1. ES6 class 静态方法:清晰的面向对象实现

ES6 的 class 语法让面向对象编程在 JavaScript 中更加直观。我们可以利用静态属性来持有实例,静态方法来控制实例的创建和获取。

JavaScript

class Storage {
    // 静态属性,用于存储唯一的实例
    static #instance = null; // 使用私有字段 #instance,提高封装性

    constructor() {
        // 如果实例已存在,直接返回现有实例,防止通过 new 关键字意外创建
        if (Storage.#instance) {
            return Storage.#instance;
        }
    }

    // 静态方法:获取 Storage 的唯一实例
    static getInstance() {
        if (!Storage.#instance) {
            Storage.#instance = new Storage();
        }
        return Storage.#instance;
    }

    // 封装 localStorage.getItem
    getItem(key) {
        return localStorage.getItem(key);
    }

    // 封装 localStorage.setItem
    setItem(key, value) {
        // 考虑存储非字符串类型,可以使用 JSON.stringify
        localStorage.setItem(key, JSON.stringify(value));
    }

    // 封装 localStorage.removeItem
    removeItem(key) {
        localStorage.removeItem(key);
    }

    // 封装 localStorage.clear
    clear() {
        localStorage.clear();
    }
}

// 获取 Storage 的唯一实例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();

console.log(storage1 === storage2); // 输出: true,验证是同一个实例

storage1.setItem('userProfile', { name: 'Alice', age: 30 });
console.log(storage2.getItem('userProfile')); // 输出: "{"name":"Alice","age":30}"
// 注意:从 localStorage 取出的值是字符串,需要手动解析
console.log(JSON.parse(storage1.getItem('userProfile')).name); // 输出: Alice

代码解析:

  • static #instance = null; : 定义一个私有静态字段#instance,它属于 Storage 类本身,用于存储唯一的实例。# 前缀确保了它的私有性,外部无法直接访问。
  • constructor() : 构造函数内部检查 #instance,如果已存在,则直接返回现有实例。这是一种额外的保护,防止在 getInstance 之外直接使用 new Storage() 创建新实例。
  • static getInstance() : 这是获取 Storage 实例的唯一入口。它首次调用时创建实例并赋值给 #instance,后续调用则直接返回已存在的实例。
  • 方法封装: 对 localStoragegetItemsetItem 等方法进行了封装,并考虑了 JSON.stringifyJSON.parse 来处理非字符串数据类型,增强了实用性。

2. 闭包实现:私有化与惰性加载的经典结合

闭包是 JavaScript 的强大特性,它能创建私有作用域,非常适合实现单例模式,尤其是当需要惰性加载(即只在首次需要时才创建实例)时。

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>
        // 核心功能类,承载 localStorage 操作
        function StorageCore() {
            // 构造函数可以进行一些初始化
        }

        StorageCore.prototype.getItem = function (key) {
            const value = localStorage.getItem(key);
            // 尝试解析 JSON,如果失败则返回原始值
            try {
                return JSON.parse(value);
            } catch (e) {
                return value;
            }
        };

        StorageCore.prototype.setItem = function (key, value) {
            localStorage.setItem(key, JSON.stringify(value));
        };

        StorageCore.prototype.removeItem = function (key) {
            localStorage.removeItem(key);
        };

        StorageCore.prototype.clear = function () {
            localStorage.clear();
        };

        // 使用立即执行函数表达式 (IIFE) + 闭包实现单例
        const GlobalStorage = (function() {
            let instance = null; // 被闭包私有化的实例变量

            // 返回一个函数,这个函数作为外部获取单例的接口
            return function() {
                if (!instance) {
                    instance = new StorageCore(); // 首次调用时创建实例
                }
                return instance; // 返回唯一的实例
            };
        })();

        // 使用 GlobalStorage 函数来获取单例
        const storageA = new GlobalStorage();
        const storageB = new GlobalStorage();

        console.log(storageA === storageB); // 输出: true

        storageA.setItem('appName', 'MyWebApp');
        console.log(storageB.getItem('appName')); // 输出: MyWebApp (已自动解析)
    </script>
</body>
</html>

代码解析:

  • StorageCore: 定义一个包含实际 localStorage 操作的原型式类。为了更健壮,getItem 方法增加了 try...catch 块来处理非 JSON 字符串的情况,setItem 默认进行 JSON.stringify
  • const GlobalStorage = (function() { ... })(); : 这是一个立即执行函数表达式 (IIFE) 。它创建了一个独立的作用域。
  • let instance = null; : 在 IIFE 内部声明的 instance 变量,对外部是不可见的,但被 IIFE 返回的函数所“捕获”,形成了闭包。这是实现私有性的关键。
  • return function() { ... } : IIFE 返回一个函数,这个函数就是我们对外暴露的 GlobalStorage 构造器。每次 new GlobalStorage() 实际上都是在调用这个返回的函数。
  • 单例逻辑: 在返回的函数内部,每次调用都会检查 instance 是否已存在。如果不存在,则创建一个 StorageCore 实例并赋值给 instance;如果已存在,则直接返回 instance

面试制胜秘籍:单例模式的考点与应用

在面试中,考官不仅会关注你对单例模式的实现能力,还会深入考察你对其优缺点应用场景的理解。

常见考点:

  1. 概念阐述: 什么是单例模式?它的核心思想是什么?
  2. 实现方式: 至少能写出一种(最好两种)单例模式的实现代码,并解释其原理。
  3. 优势分析: 单例模式能解决哪些问题?带来哪些好处?(如性能、资源管理、状态一致性)
  4. 局限性讨论: 单例模式有哪些潜在缺点?(例如,可能增加代码耦合度,对单元测试不够友好,因为全局状态难以隔离)
  5. 实际应用: 在哪些场景下会使用单例模式?除了本文的 localStorage 封装,还能想到其他例子吗?

应用场景举例:

  • 数据缓存: 如文中的 localStorage 封装,确保应用程序的任何部分都通过同一个实例读写本地存储。
  • 全局状态管理: 在大型前端框架(如 Vuex、Redux)中,Store 通常就是以单例形式存在的。
  • 日志系统: 确保所有日志都通过同一个日志记录器写入,便于统一管理和追踪。
  • 配置管理器: 全局配置对象,保证应用程序读取的配置始终是最新且唯一的。
  • 模态框/弹窗管理器: 确保屏幕上同一时间只有一个模态框被控制。