Skip to content

3.1 原型模式 (Prototype Pattern)

原型模式提供了一种非常独特的创建对象的方式:它不是通过 new 关键字从类中创建实例,而是通过复制(克隆)一个已有的对象来创建新的对象。

核心定义:使用一个“原型”实例来指定要创建的对象的类型,然后通过复制这个原型来创建新的对象。

一、生活中的场景:克隆羊多莉

你一定听说过克隆羊多莉。科学家们不是从一个受精卵开始,一步步培育一只全新的羊。他们走了一条捷径:

  1. 找到一只成年母羊(原型)。
  2. 提取出它的细胞。
  3. 通过一系列复杂的技术,复制这个细胞,并最终培育出一个和原型羊基因完全相同的新个体——多莉(新对象)。

这个“克隆”过程,就是原型模式的精髓。当从零开始创建一个对象非常复杂耗时的时候,克隆一个现成的模板会高效得多。

二、没有原型模式的代码:昂贵的创建成本

假设我们在开发一款游戏,需要在一个关卡中生成 100 只相同的怪物。每只怪物都需要加载庞大的 3D 模型、纹理和音效,这是一个非常耗时的过程。

javascript
class Monster {
  constructor(name) {
    this.name = name;
    // 模拟一个非常耗时的初始化过程
    this.init();
  }

  init() {
    console.log(`--- ${this.name} 正在加载资源... ---`);
    // 假设这里有 1 秒的延迟来加载模型、纹理等
    const startTime = new Date().getTime();
    while (new Date().getTime() < startTime + 1000); // 阻塞 1 秒
    console.log(`--- ${this.name} 资源加载完成! ---`);
  }

  show() {
    console.log(`我是怪物:${this.name}`);
  }
}

// =======================================================
// 创建一个怪物军团
console.time("创建军团耗时");
const monsterArmy = [];
for (let i = 0; i < 100; i++) {
  // 每次都 new 一个,每次都要走一遍完整的、耗时的 init 过程
  const monster = new Monster(`哥布林 #${i}`);
  monsterArmy.push(monster);
}
console.timeEnd("创建军团耗时"); // 在浏览器中,你会看到一个非常夸张的时间

// 问题:创建 100 只怪物,init() 被完整执行了 100 次!
// 这对于游戏性能来说是不可接受的。

三、使用原型模式重构

我们的目标是:昂贵的 init 过程只执行一次,后续的 99 只怪物都通过“克隆”来快速创建。

在 JavaScript 中,实现原型模式最地道的方式是使用 Object.create()

Object.create(proto) 方法会创建一个新对象,并使用现有的对象 proto 作为新创建的对象的 [[Prototype]](即 __proto__)。

javascript
// 怪物类,但我们把 init 过程放到了一个静态方法中,用于创建“原型”
class Monster {
  constructor(name, abilities) {
    this.name = name;
    this.abilities = abilities;
  }

  clone() {
    // Object.create 会创建一个新对象,其原型指向 this
    // 这样,克隆出来的对象就能继承 Monster 的所有方法
    const clone = Object.create(this);
    // 如果有引用类型的属性,需要特别处理(见下一节)
    return clone;
  }

  show() {
    console.log(`我是怪物:${this.name},我会 ${this.abilities.join(", ")}`);
  }
}

// =======================================================
// 1. 创建一个“原型”怪物,这个过程只发生一次!
console.log("正在创建原型怪物...");
const goblinPrototype = new Monster("哥布林", ["敲击", "投石"]);
// 假设这里有一个昂贵的初始化过程
// goblinPrototype.init();
console.log("原型创建完成!");

// 2. 使用原型来克隆怪物军团
console.time("克隆军团耗时");
const monsterArmy = [];
for (let i = 0; i < 100; i++) {
  const monster = goblinPrototype.clone();
  // 为每个克隆体设置自己独特的属性
  monster.name = `哥布林 #${i}`;
  monsterArmy.push(monster);
}
console.timeEnd("克隆军团耗时"); // 时间会变得飞快!

monsterArmy.show(); // 我是怪物:哥布林 #5,我会 敲击, 投石

通过重构,昂贵的创建过程只在生成“原型”时执行了一次。后续的对象都是通过极快的内存复制来完成的,性能得到了巨大的提升。

四、核心问题:浅拷贝 vs. 深拷贝

原型模式有一个非常重要的细节需要注意。上面的 clone 方法实现的是浅拷贝

让我们来看一个例子:

javascript
const monster1 = goblinPrototype.clone();
const monster2 = goblinPrototype.clone();

// 修改 monster1 的装备
monster1.abilities.push("冲锋");

console.log(monster1.abilities); // [ '敲击', '投石', '冲锋' ]
console.log(monster2.abilities); // [ '敲击', '投石', '冲锋' ]
console.log(goblinPrototype.abilities); // [ '敲击', '投石', '冲锋' ]

// 灾难!所有怪物共享了同一个 abilities 数组!

为什么会这样? 因为 Object.create() 只创建了一个新对象,但原型对象上的属性如果是引用类型(如 Object, Array),新对象只是复制了这个引用的“地址”,而不是复制“值”本身。所有克隆体内部的 abilities 属性都指向内存中同一个数组。

解决方案:深拷贝 (Deep Copy) 我们需要在 clone 时,对所有引用类型的属性进行递归的、彻底的复制。

javascript
// 一个更健壮的 clone 方法
Monster.prototype.clone = function () {
  const clone = Object.create(this);

  // 对引用类型的属性进行深拷贝
  // 注意:JSON.stringify/parse 是最简单的深拷贝方法,但有局限性
  // (例如,无法拷贝函数、Date 对象会变成字符串等)
  // 在实际项目中,可能会使用更完善的库,如 lodash 的 _.cloneDeep
  clone.abilities = JSON.parse(JSON.stringify(this.abilities));

  return clone;
};

// 现在再试一次
const newPrototype = new Monster("兽人", ["咆哮"]);
const orc1 = newPrototype.clone();
const orc2 = newPrototype.clone();

orc1.abilities.push("劈砍");

console.log(orc1.abilities); // [ '咆哮', '劈砍' ]
console.log(orc2.abilities); // [ '咆哮' ]  ✅ 成功隔离!

五、优缺点与适用场景

优点

  1. 性能卓越:当创建对象的成本很高时(耗时、耗资源),原型模式通过克隆来规避这些成本,性能优势巨大。
  2. 简化创建:简化了复杂对象的创建过程,因为你可以通过复制一个已配置好的对象来得到新对象。

缺点

  1. 需要实现 clone 方法:每个需要被克隆的类都必须配备一个 clone 方法,并且需要谨慎地处理深拷贝和浅拷贝的问题。
  2. 可能破坏封装clone 方法可能会需要访问对象内部的私有属性,这可能会破坏其封装性。

适用场景

  • 当一个对象的创建过程非常耗时或消耗大量资源时。
  • 当需要创建大量相似的对象,它们之间只有少量属性不同时。
  • 在一个系统中,已经存在一个对象,而你又需要一个跟它状态完全一样的新对象时。