Skip to content

参考

基于 Refactoring.Guru 设计模式官方教程、MDN(Object.create / structuredClone)与 Addy Osmani《Learning JavaScript Design Patterns》编写,对照 ES2022 + TS 4.3+ 稳定行为

速查

  • 五种创建型模式:工厂方法 / 抽象工厂 / 建造者 / 原型 / 单例——核心都是「抽象对象实例化」
  • 工厂方法 vs 抽象工厂:继承 + 单一产品 vs 组合 + 一族产品
  • 建造者 vs 抽象工厂:末尾 getResult() 一次返回 vs 立即返回产品
  • 单例三要素:私有静态 instance + 私有构造函数 + 公有静态 getInstance()
  • JS 原生 API:Object.create(proto, desc) 原型模式、structuredClone(value, {transfer}) 深拷贝、展开 {...obj} 浅拷贝
  • 前端工厂实践:React.createElement(type, props, ...children)、Vue 3 h(type, props, children)
  • 单例最佳实践:ES Module export const config > 手写 Singleton 类
  • structuredClone 不可克隆:函数 / DOM 节点 / Symbol / 描述符;不保留原型链
  • Object.create 属性默认全 false:writable / enumerable / configurable
  • 完整说明见 入门 / 五种模式深度讲解

五模式对比表

模式意图关键角色创建时机复杂度适用场景
工厂方法子类决定实例化哪种产品Creator / ConcreteCreator / Product子类重写工厂方法时低(4 角色)单一产品类型、希望扩展时不改客户端
抽象工厂创建一族相关产品并保证一致GUIFactory / WinFactory / MacFactory工厂方法立即返回高(4×N 角色)多平台 / 多主题 / 一套兼容组件族
建造者分步装配复杂对象Builder / Director(可选) / Product所有步骤完成后 getResult()中(4 角色)多参数 / 多步骤 / 有标准构造流程
原型克隆已有实例clone(): T + PrototypeRegistry克隆时(已存在原型)创建成本高、子类数量爆炸时替代工厂方法
单例全局唯一实例private constructor + getInstance()首次调用 getInstance()低(1 类)数据库连接 / 配置 / 全局状态

工厂方法 vs 抽象工厂

维度工厂方法抽象工厂
创建维度单一产品一族相关产品
实现机制继承(Creator 子类)组合(客户端持工厂)
扩展产品类型加 ConcreteCreator 子类(开闭友好)改所有工厂接口(开闭违反)
扩展产品种类不适用加新产品种类 = 改接口(违反开闭)
复杂度4 角色4 × N 产品种类
典型场景Dialog + 不同 ButtonWinFactory / MacFactory 整族 UI

建造者 vs 抽象工厂

维度建造者抽象工厂
创建方式分步 setXxx + 末尾 getResult()立即返回完整产品
返回时机步骤完成后调用工厂方法即返回
关注点复杂对象的构造过程整族产品的一致性
Director可选,封装标准流程
典型场景多字段 Car / 表单 / HTTP 请求多主题 UI 组件库

原型模式 JS 原生 API

API类型行为保留原型链支持循环引用备注
Object.create(proto, desc)原型链继承以 proto 为 [[Prototype]] 创建新对象是(沿链查找)N/ApropertiesObject 属性默认全 false
structuredClone(value, {transfer})深拷贝递归复制全部字段(原型方法丢失)2022 起全浏览器可用
{...obj} / [...arr]浅拷贝复制顶层字段,引用字段共享简单对象常用
JSON.parse(JSON.stringify(obj))深拷贝走 JSON 序列化抛错丢 Date / Map / Set / RegExp / undefined
自实现 clone()深拷贝在构造函数里复制取决于实现类实例推荐方式

structuredClone 能力边界

支持

  • 循环引用(自动处理)
  • Date / RegExp / Error(部分浏览器)
  • Map / Set / WeakMap(键值都被克隆)/ WeakSet
  • TypedArray / ArrayBuffer / DataView
  • Boolean / String / Number / BigInt 包装对象
  • transferable 转移(ArrayBuffer / MessagePort / ImageBitmap)

不支持(抛 DataCloneError 或返回替代值):

  • 函数 → 抛 DataCloneError
  • DOM 节点 → 抛 DataCloneError
  • Symbol(属性键 OK,但 Symbol 值不行)
  • 属性描述符 / setter / getter
  • 类实例的原型链 → 不抛错,但 instanceof 失败、原型方法变 undefined

单例实现对比

方式实现优点缺点
手写 Singleton 类private constructor + getInstance()强制私有构造,TS 编译期报错测试 mock 难、隐藏依赖
ES Module 单例export const config = {...}模块仅求值一次、tree-shaking 友好、懒加载不能强制私有构造
对象字面量const logger = {...} as const最简单不能延迟初始化
Symbol 防重复Symbol.for("logger") 作 key防止重复实例化较少用,多用于底层库
全局变量(window.x)window.config = {...}全局可见污染全局、被覆盖风险高

前端框架工厂实践

React.createElement

ts
// 工厂函数签名——type + props + children
React.createElement(
  type: string | React.ComponentType,
  props?: Record<string, unknown>,
  ...children: React.ReactNode[]
): React.ReactElement

返回 React 元素(即 vnode 的 React 实现),调用方从不直接 new ReactElement——后续 React reconciler 根据 type 创建真实 DOM 或组件实例。

Vue 3 h()

ts
import { h } from "vue";

// h 是 hyperscript(生成 HTML 的脚本)的简称
h(
  type: string | Component,
  props?: Record<string, unknown>,
  children?: string | VNode[]
): VNode

返回 Vue 的 vnode 对象,调用方不直接 new VNode——后续 Vue patch 算法根据 vnode 创建真实 DOM。

Pinia / Vuex store 单例

ts
// Pinia——useXxxStore() 在应用生命周期内返回同一 store 实例
import { useUserStore } from "@/stores/user";

const userStore = useUserStore();
// 任意组件、任意时机调用拿到的都是同一实例——单例模式的状态管理落地

Vue inject / provide

ts
// 父级 provide 一次,所有子级 inject 拿到同一对象——单例语义
import { provide, inject } from "vue";

const ThemeKey = Symbol("theme");

// 父组件
provide(ThemeKey, { mode: "dark" });

// 任意深度的子组件
const theme = inject(ThemeKey);

反模式速查

反模式后果修复
God Object 单例隐藏依赖中心,测试难 mock拆成多个独立模块
JSON.parse(JSON.stringify) 深拷贝丢 Date / Map / Set、循环引用抛错用 structuredClone
structuredClone 类实例后期望保留方法原型方法 undefined、instanceof 失败自实现 clone()
Object.create(42) 等非对象 proto抛 TypeError传对象或 null
Object.create({}, { p: { value: 42 } })p 默认全 false,赋值静默失败显式声明描述符
简单产品上抽象工厂接口爆炸、违反 YAGNI用简单工厂函数
JS 写双重检查锁(DCL)多余噪音用 Promise 缓存解决异步竞态
工厂方法返回具体类型改产品要改客户端返回产品接口
Builder 强加 Director徒增类数量简单场景直接链式调用

官方资源