设计模式-2(单例模式与原型模式)

目录

[3. 单例模式](#3. 单例模式)

[1. 核心定义](#1. 核心定义)

[2. 单例模式的两种经典实现](#2. 单例模式的两种经典实现)

[3. 单例模式在Vuex中的实践](#3. 单例模式在Vuex中的实践)

[1. 为什么Store是"假单例"?](#1. 为什么Store是“假单例”?)

[2. Vuex如何保证"应用内Store唯一"?](#2. Vuex如何保证“应用内Store唯一”?)

[3. "假单例"的局限性](#3. “假单例”的局限性)

小结

[4. 实践案例:全局弹窗管理器](#4. 实践案例:全局弹窗管理器)

场景说明

4.原型模式

[1. 原型模式的定位:JS中的"编程范式"而非"可选设计模式"](#1. 原型模式的定位:JS中的“编程范式”而非“可选设计模式”)

[2. 类中心语言 vs 原型中心语言对比](#2. 类中心语言 vs 原型中心语言对比)

[3. 原型与原型链:JS原型范式的核心](#3. 原型与原型链:JS原型范式的核心)

[3.1 原型的三大核心关系](#3.1 原型的三大核心关系)

[3.2 原型链的工作机制](#3.2 原型链的工作机制)

[4. 关键实践:对象深拷贝](#4. 关键实践:对象深拷贝)

[4.1 深拷贝的核心需求](#4.1 深拷贝的核心需求)

[4.2 两种常见实现方式](#4.2 两种常见实现方式)

[5. 案例练习:基于原型模式创建"学生对象"并实现深拷贝](#5. 案例练习:基于原型模式创建“学生对象”并实现深拷贝)

需求描述

3. 单例模式

1. 核心定义

单例模式是保证一个类仅有一个实例,并提供全局唯一访问点的设计模式。它的核心价值在于:

  • 避免重复创建实例导致的内存浪费(如全局状态容器、弹窗管理器);
  • 确保全局范围内实例的一致性(如配置对象、工具类)。
    该模式是前端面试高频考点,且在Vuex、Redux、jQuery等主流库中广泛应用。

2. 单例模式的两种经典实现

核心思路:通过"拦截实例创建"逻辑,确保无论调用多少次,仅返回首次创建的实例。

通过类的静态属性存储唯一实例,静态方法 getInstance 控制实例的创建与返回:

复制代码
class SingleDog {
  show() {
    console.log('我是全局唯一的单例实例');
  }

  // 静态方法:全局访问入口
  static getInstance() {
    // 1. 判断实例是否已存在
    if (!SingleDog.instance) {
      // 2. 不存在则创建实例,挂载到类的静态属性上
      SingleDog.instance = new SingleDog();
    }
    // 3. 存在则直接返回
    return SingleDog.instance;
  }
}

// 测试:两次调用返回同一实例
const dog1 = SingleDog.getInstance();
const dog2 = SingleDog.getInstance();
console.log(dog1 === dog2); // true(证明实例唯一)

利用闭包的"私有变量"特性,隐藏实例状态(避免全局污染),仅暴露访问方法:

复制代码
class SingleDog {
  show() {
    console.log('我是闭包维护的单例实例');
  }
}

// 立即执行函数(IIFE)创建闭包,保存实例状态
SingleDog.getInstance = (function() {
  // 闭包内的私有变量:仅内部可修改,外部无法访问
  let instance = null;

  // 返回访问函数(全局访问入口)
  return function() {
    if (!instance) {
      instance = new SingleDog();
    }
    return instance;
  };
})();

// 测试:结果仍为同一实例
const dog1 = SingleDog.getInstance();
const dog2 = SingleDog.getInstance();
console.log(dog1 === dog2); // true

3. 单例模式在Vuex中的实践

Vuex的Store(全局状态容器)是单例模式的典型应用,但并非"严格单例",而是"假单例" ------即"没有在Store类内部实现单例逻辑,却通过Vuex的整体设计保证同一Vue应用中Store唯一"。

1. 为什么Store是"假单例"?

Store类的构造函数无任何单例拦截逻辑 ,开发者可通过new关键字创建多个实例:

复制代码
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 手动创建两个不同的Store实例
const store1 = new Vuex.Store({ state: { count: 0 } });
const store2 = new Vuex.Store({ state: { count: 0 } });

console.log(store1 === store2); // false(证明Store类本身不限制实例数量)
2. Vuex如何保证"应用内Store唯一"?

Vuex通过两个核心逻辑,从"应用层面"确保Store唯一:

  • install函数:拦截多次插件安装
    Vuex是Vue插件,需通过Vue.use(Vuex)安装。install函数会判断Vue应用是否已安装过Vuex,若已安装则直接返回,避免重复注入:

    let Vue; // 存储已安装Vuex的Vue实例

    export function install(_Vue) {
    // 若已安装,直接拦截
    if (Vue && _Vue === Vue) {
    console.error('[vuex] 已安装,Vue.use(Vuex)只需调用一次');
    return;
    }
    // 未安装则标记,后续不再重复安装
    Vue = _Vue;
    // 注入Store到Vue实例(下一步逻辑)
    applyMixin(Vue);
    }

  • vuexInit函数:组件树继承 $store
    applyMixin会在Vue的beforeCreate生命周期注入vuexInit,让所有子组件继承根组件的$store,确保全应用访问同一实例:

    function vuexInit() {
    const options = this.options; // 根组件:直接挂载Store if (options.store) { this.store = options.store;
    }
    // 子组件:继承父组件的store else if (options.parent && options.parent.store) {
    this.store = options.parent.store;
    }
    }

3. "假单例"的局限性

Store的唯一性仅针对同一Vue应用 :若页面中存在多个Vue应用(如多个new Vue()),每个应用可拥有独立的Store实例。若需跨应用共享Store,仍需手动用单例模式封装。

小结

  1. 单例模式的核心是"唯一实例+全局访问",实现关键是"拦截实例创建";
  2. Vuex的Store是"假单例":通过install拦截重复安装、vuexInit继承$store,确保应用内唯一;
  3. 单例模式的价值在于"节省内存+保证一致性",适合全局工具类、状态容器、弹窗管理器等场景。

4. 实践案例:全局弹窗管理器

场景说明

前端应用中,弹窗(如提示框、确认框)通常需要"全局唯一"(避免多个弹窗叠加),适合用单例模式实现------确保无论调用多少次show,仅显示一个弹窗,且全局可通过统一方法控制。

复制代码
// 全局弹窗管理器:单例模式(闭包实现)
const PopupManager = (function() {
  // 私有变量:存储弹窗DOM、实例状态
  let popupDOM = null;
  let instance = null;

  // 私有方法:创建弹窗DOM(仅内部调用)
  function createPopupDOM() {
    const div = document.createElement('div');
    div.style.cssText = `
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
      padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 4px;
      z-index: 9999; display: none;
    `;
    document.body.appendChild(div);
    return div;
  }

  // 公有的实例方法
  function Popup() {
    this.popupDOM = popupDOM || createPopupDOM();
  }

  // 显示弹窗:接收弹窗内容
  Popup.prototype.show = function(content) {
    this.popupDOM.innerText = content;
    this.popupDOM.style.display = 'block';
  };

  // 隐藏弹窗
  Popup.prototype.hide = function() {
    this.popupDOM.style.display = 'none';
  };

  // 全局访问入口:确保实例唯一
  return {
    getInstance: function() {
      if (!instance) {
        instance = new Popup();
      }
      return instance;
    }
  };
})();

// 测试:两次调用getInstance,操作同一弹窗
const popup1 = PopupManager.getInstance();
const popup2 = PopupManager.getInstance();

popup1.show('这是第一个弹窗'); // 显示弹窗
setTimeout(() => {
  popup2.hide(); // 隐藏弹窗(证明操作的是同一实例)
}, 2000);

// 定义弹窗管理器的类型接口
interface IPopupManager {
  show(content: string): void;
  hide(): void;
}

class PopupManager implements IPopupManager {
  // 静态属性:存储唯一实例
  private static instance: PopupManager | null = null;
  // 私有属性:存储弹窗DOM
  private popupDOM: HTMLDivElement | null = null;

  // 私有构造函数:禁止外部通过new创建实例
  private constructor() {
    this.popupDOM = this.createPopupDOM();
  }

  // 私有方法:创建弹窗DOM
  private createPopupDOM(): HTMLDivElement {
    const div = document.createElement('div');
    div.style.cssText = `
      position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
      padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 4px;
      z-index: 9999; display: none;
    `;
    document.body.appendChild(div);
    return div;
  }

  // 静态方法:全局访问入口
  public static getInstance(): PopupManager {
    if (!PopupManager.instance) {
      PopupManager.instance = new PopupManager();
    }
    return PopupManager.instance;
  }

  // 显示弹窗(实现IPopupManager接口)
  public show(content: string): void {
    if (this.popupDOM) {
      this.popupDOM.innerText = content;
      this.popupDOM.style.display = 'block';
    }
  }

  // 隐藏弹窗(实现IPopupManager接口)
  public hide(): void {
    if (this.popupDOM) {
      this.popupDOM.style.display = 'none';
    }
  }
}

// 测试:TypeScript类型安全的单例调用
const popup1 = PopupManager.getInstance();
const popup2 = PopupManager.getInstance();

popup1.show('TypeScript单例弹窗'); // 类型提示:content需为string
setTimeout(() => {
  popup2.hide(); // 操作同一弹窗实例
}, 2000);

// 错误:构造函数是私有的,禁止外部new
// const popup3 = new PopupManager(); // TS编译报错

4.原型模式

1. 原型模式的定位:JS中的"编程范式"而非"可选设计模式"

核心差异:Java等类中心语言 中,原型模式是"特定场景工具"(仅当需要克隆对象避免重复传参时使用);而在JS中,原型模式是"面向对象的根基"------所有对象的创建、继承都依赖原型,是必选的编程范式

关键澄清:

  • JS使用原型的目的不是"克隆副本",而是"共享数据/方法+创建对应类型实例";
  • ES6 class 是原型继承的语法糖 (MDN官方定义),并非全新的类模型,本质仍依赖 Prototype

2. 类中心语言 vs 原型中心语言对比

|----------|-----------------------------|--------------------------------------|
| 对比维度 | 类中心语言(如Java) | 原型中心语言(如JavaScript) |
| 对象创建基础 | 必须通过"类实例化"(new 类名(...)) | 通过"原型克隆"(Object.create/new 构造函数) |
| 原型模式的角色 | 可选工具(仅克隆场景用) | 必选范式(所有对象依赖原型) |
| 重复对象创建方式 | 重复传入相同参数(如两次new Dog(...)) | 克隆原型对象(无需重复传参) |

3. 原型与原型链:JS原型范式的核心

3.1 原型的三大核心关系
  • 构造函数 :拥有 prototype 属性,指向"原型对象";

  • 原型对象 :拥有 constructor 属性,指回"构造函数",可挂载共享方法/属性;

  • 实例对象 :拥有 __proto__ 属性(非标准但浏览器通用),创建时自动指向"构造函数的原型对象"。

    // 构造函数
    function Dog(name) { this.name = name; }
    // 原型对象挂载共享方法
    Dog.prototype.bark = () => console.log('汪汪');
    // 实例对象
    const wangcai = new Dog('旺财');

    // 核心关系验证
    console.log(wangcai.proto === Dog.prototype); // true(实例→原型对象)
    console.log(Dog.prototype.constructor === Dog); // true(原型对象→构造函数)

3.2 原型链的工作机制
  • 定义:访问实例的属性/方法时,会按以下顺序搜索,形成的轨迹即"原型链":
    实例本身 → 实例. proto (原型对象) → 原型对象. proto (原型的原型) → ... → Object.prototype(链的顶端)
    若搜索至顶端仍未找到,返回 undefined

示例:wangcai.toString() 的执行逻辑:

  1. wangcai 实例:无 toString 方法;
  2. Dog.prototype:无 toString 方法;
  3. Object.prototype:存在 toString,执行该方法。

4. 关键实践:对象深拷贝

4.1 深拷贝的核心需求

解决"引用类型共享问题":浅拷贝仅复制引用(如数组、对象),修改副本会影响原对象;深拷贝复制所有层级数据,副本与原对象完全独立。

4.2 两种常见实现方式

取巧方案JSON.stringify + JSON.parse

  • 优点:代码简洁,适合"纯JSON对象"(无函数、正则、循环引用);

  • 缺点:无法处理 functionRegExpDate、循环引用等场景。

    function deepClone(obj) {
    // 处理值类型或null
    if (typeof obj !== 'object' || obj === null) return obj;
    // 区分数组和普通对象
    const copy = Array.isArray(obj) ? [] : {};
    // 遍历自有属性并递归拷贝
    for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
    copy[key] = deepClone(obj[key]);
    }
    }
    return copy;
    }

核心逻辑:值类型(如数字、字符串)直接返回;引用类型(对象/数组)递归遍历,拷贝自有属性。

5. 案例练习:基于原型模式创建"学生对象"并实现深拷贝

需求描述
  1. 定义"学生"类型:包含 name(姓名)、age(年龄)、hobbies(爱好,数组)属性,以及 study(学习)方法(所有学生共享该方法,避免重复创建);

  2. 创建学生实例(如"小红",爱好为 ['阅读', '编程']);

  3. 用深拷贝复制该实例,修改副本的 hobbies(如删除"阅读"),验证原实例与副本的引用类型属性是否独立。

    // 1. 定义学生构造函数(原型挂载共享方法)
    function Student(name, age, hobbies) {
    this.name = name;
    this.age = age;
    this.hobbies = hobbies;
    }

    // 共享方法:所有学生共用,不重复创建
    Student.prototype.study = function() {
    console.log(${this.name}正在学习,目标是掌握原型模式!);
    };

    // 2. 实现深拷贝函数
    function deepClone(obj) {
    if (typeof obj !== 'object' || obj === null) return obj;
    const copy = Array.isArray(obj) ? [] : {};
    for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
    copy[key] = deepClone(obj[key]);
    }
    }
    return copy;
    }

    // 3. 测试逻辑
    const xiaohong = new Student('小红', 17, ['阅读', '编程']);
    const xiaohongCopy = deepClone(xiaohong);

    // 修改副本的爱好(删除"阅读")
    xiaohongCopy.hobbies.splice(0, 1);

    // 验证独立性:原实例与副本的hobbies互不影响
    console.log('原实例爱好:', xiaohong.hobbies); // 输出:['阅读', '编程']
    console.log('副本爱好:', xiaohongCopy.hobbies); // 输出:['编程']

    // 验证原型方法共享
    xiaohong.study(); // 输出:小红正在学习,目标是掌握原型模式!
    xiaohongCopy.study(); // 输出:小红正在学习,目标是掌握原型模式!

    // 1. 用 class 简化"原型+类型":自带构造函数、原型方法、类型约束
    class Student {
    // 直接声明属性(自动关联类型,不用单独写 interface)
    name: string;
    age: number;
    hobbies: string[];

    // 构造函数(不用手动定义 StudentConstructor 类型)
    constructor(name: string, age: number, hobbies: string[]) {
    this.name = name;
    this.age = age;
    this.hobbies = hobbies;
    }

    // 原型方法(自动挂载到 Student.prototype,不用手动赋值)
    study() {
    console.log(${this.name}正在学习,目标是掌握原型模式!);
    }
    }

    // 2. 简化泛型深拷贝(减少冗余断言,利用类型守卫)
    function deepClone<T>(obj: T): T {
    // 处理值类型/null:直接返回(TS 自动推导类型)
    if (typeof obj !== 'object' || obj === null) return obj;

    // 区分数组/对象:用类型守卫减少 as 断言
    const copy = Array.isArray(obj)
    ? obj.map(item => deepClone(item)) // 数组:递归拷贝每一项
    : Object.fromEntries( // 对象:遍历键值对递归拷贝
    Object.entries(obj).map(([key, val]) => [key, deepClone(val)])
    ) as T;

    return copy;
    }

    // 3. 测试逻辑(和原代码完全一致,功能不变)
    const xiaohong = new Student('小红', 17, ['阅读', '编程']);
    const xiaohongCopy = deepClone(xiaohong);

    // 修改副本不影响原对象(深拷贝生效)
    xiaohongCopy.hobbies.splice(0, 1);
    console.log('原实例爱好:', xiaohong.hobbies); // ['阅读', '编程']
    console.log('副本爱好:', xiaohongCopy.hobbies); // ['编程']

    // 原型方法共享(class 自动挂载到原型)
    xiaohong.study(); // 小红正在学习...
    xiaohongCopy.study(); // 小红正在学习...

相关推荐
bugcome_com2 小时前
ASP.NET Web Pages 教程 —— Razor 语法全面指南
前端·asp.net
霍理迪2 小时前
Vue—侦听属性
前端·javascript·vue.js
酉鬼女又兒2 小时前
零基础入门前端弹性布局(Flexbox)实战:结合 Class 与 ID 选择器(可用于备赛蓝桥杯Web开发应用)
前端·css·蓝桥杯·html·html5
小J听不清2 小时前
CSS display 属性全解析:块级 / 行内 / 行内块 / 隐藏
前端·javascript·css·html·css3
早點睡3902 小时前
ReactNative项目Openharmony三方库集成实战:react-native-safe-area-context
javascript·react native·react.js
ONLYOFFICE2 小时前
ONLYOFFICE 全新 PDF 编辑器 API 上线,自动化处理 PDF 内容
前端·人工智能·pdf·编辑器·onlyoffice
James man2 小时前
前端节点连接库选型指南:React-Flow、AntV X6 与 Power-Link 深度对比
前端·react.js·前端框架
砍光二叉树2 小时前
【设计模式】创建型-单例模式
单例模式·设计模式
于慨2 小时前
java Web
java·开发语言·前端