手撕大厂面试题:用单例模式实现 Storage

82 阅读5分钟

面试题

实现Storage,使得该对象为单例,基于localStorage进行封装。实现方法 setItem(key,value) 和 getItem(key)。

分析题目

  • 实现Storage:实现Storage
  • 使得该对象为单例:使用单例模式
  • 基于localStorage进行封装,实现方法 setItem(key,value)getItem(key):浏览器本地存储

所需技能点

JavaScript 中的类

什么是类?

  • 类是一种模板(蓝图) ,用于创建对象。
  • 它是 ES6 引入的语法糖,让面向对象编程更清晰。

为什么JS需要类?

在ES6前,JS并没有class关键字,所谓的“类”,其实是通过 构造函数 + prototype 原型 实现的,例如:

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function () {
    console.log("Hello, " + this.name);
};

const p = new Person("Tom");
p.sayHello(); // Hello, Tom

JavaScript是一门基于原型的语言,每个对象都有一个原型__proto__,它指向另一个对象,从而实现继承和共享方法。所以不需要像JAVA那样有复杂的类结构,直接操作对象就可以。

所以有人说:“在 JS 中,根本不需要学面向对象。”

那为什么要在JavaScript中设计类?

因为ES6后要推动JavaScript进入企业级、大型项目的开发领域,为了让更多的传统面向对象开发者更容易上手JavaScript,像 Java/C++ 程序员难以理解 JS 的原型机制:

  • Java 和 C++ 是典型的基于类的语言。
  • 它们的继承是通过类与类之间的关系定义的。
  • 而 JS 的继承是通过对象之间的原型链实现的,这种思维方式完全不同。

因此很多从 Java/C++ 转过来的开发者会觉得 JS 的原型机制“奇怪”。

为了兼容性和可读性,ES6 引入了class

但是class是语法糖,底层依旧是原型机制。

总结一句话: JavaScript 的核心是原型式编程class 只是语法糖,目的是让更多开发者容易接受这门语言,走向企业级开发。真正理解 JS,要回到原型的本质。

基本结构

class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log('Hello, ' + this.name);
  }
}
  • constructor:构造函数,初始化对象属性。
  • 方法写在类中,会被所有实例共享

创建一个实例

const p1 = new Person('Tom');
pi.sayHello();// Hello,Tom

静态方法 static

static info() {
  console.log('This is a Person class');
}

用 类名.方法名 调用,不通过实例调用

继承

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
}
  • 使用 extends 实现继承。
  • super() 调用父类构造函数。

私有字段

class Counter {
  #count = 0;

  increment() {
    this.#count++;
  }
}

使用 # 定义私有属性或方法,外部不能访问。

单例模式 Singleton Pattern

1. 定义:

  • 单例模式是一种创建型设计模式
  • 它确保一个类在整个应用程序生命周期中只能创建一个实例,并提供一个全局访问点来访问这个实例。

2. 核心思想:

  • 类内部私有化构造函数,防止外部直接实例化。
  • 类内部维护一个静态私有实例
  • 提供一个静态方法(通常命名为 getInstance)用于获取该实例。

本地存储 localStorage

localStorage 是浏览器提供的一种本地存储数据的机制,用于在用户浏览器中持久化保存数据

主要特点:

特点说明
持久化存储数据不会因为关闭页面或浏览器而消失,除非手动清除。
同源限制只能访问和操作相同域名、端口、协议下存储的数据。
字符串存储只能保存字符串,非字符串数据(如对象)需要先用JSON.stringify()转换。注意:取内容时要用 JSON.parse()转回JSON格式
容量较大一般支持 5MB 左右的数据存储(比 cookie 大很多)。
同步操作所有读写操作都是同步的(不涉及回调或 Promise)。

常用方法

// 存储数据
localStorage.setItem('key', 'value');

// 读取数据
const value = localStorage.getItem('key');

// 删除数据
localStorage.removeItem('key');

// 清空所有数据
localStorage.clear();

示例:

// 存储
localStorage.setItem('username', 'Tom');
localStorage.setItem('user', JSON.stringify({ name: 'Tom', age: 20 }));

// 获取
const username = localStorage.getItem('username'); // "Tom"
const user = JSON.parse(localStorage.getItem('user')); // { name: 'Tom', age: 20 }

// 删除
localStorage.removeItem('username');

注意事项:

  • 不适合存储敏感信息(如密码),因为它是明文存储
  • 页面刷新后数据仍在
  • 多个标签页打开同一个网站时,数据共享

实战解析

class Storage { 
    // 静态属性
    static instance;
    // static instance 未初始化为 null 是因为在 JavaScript 中,
    // 类的静态属性若仅声明而不初始化,其默认值就是 undefined 
    // 对于单例模式而言, undefined 和 null 都能用于判断实例是否已创建。
    constructor() {
        console.log(this);
    }
    // 静态方法
    static getInstance() {
        // 静态属性 在类上直接访问的方法 不需要实例化
        if(!Storage.instance) { // 单例模式核心
            Storage.instance = new Storage(); // 只有第一次才new
        }
        return Storage.instance;
    }
    // 共享方法
    getItem(key) {
        return localStorage.getItem(key); 
    }
    setItem(key, value) {
        localStorage.setItem(key, value);
    }
}
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
// 创建的就是同一个实例
console.log(storage1 === storage2); // false -> true

为什么用单例模式封装 localStorage?

1. ❗因为 localStorage 是浏览器全局对象
  • 所有对 localStorage 的操作都是全局生效的。
  • 无论多少个 Storage 实例,底层操作的都是同一个 localStorage
2. 使用单例可以:
  • 避免重复创建实例,提高性能。
  • 统一访问接口,便于维护。
  • 确保行为一致性:所有访问都通过同一个对象进行,避免混乱。

结尾

本文介绍了单例模式的基本概念和实现方式,并结合 localStorage 封装了一个实用的 Storage 工具类。通过单例模式,我们实现了统一入口、节省资源、便于管理的本地存储方案。

JavaScript 虽然是基于原型的语言,但合理使用类和设计模式,可以提升代码结构的清晰度与可维护性。理解这些基础原理,是写出高质量前端代码的关键一步。