Appearance
3.1 原型模式 (Prototype Pattern)
原型模式提供了一种非常独特的创建对象的方式:它不是通过 new 关键字从类中创建实例,而是通过复制(克隆)一个已有的对象来创建新的对象。
核心定义:使用一个“原型”实例来指定要创建的对象的类型,然后通过复制这个原型来创建新的对象。
一、生活中的场景:克隆羊多莉
你一定听说过克隆羊多莉。科学家们不是从一个受精卵开始,一步步培育一只全新的羊。他们走了一条捷径:
- 找到一只成年母羊(原型)。
- 提取出它的细胞。
- 通过一系列复杂的技术,复制这个细胞,并最终培育出一个和原型羊基因完全相同的新个体——多莉(新对象)。
这个“克隆”过程,就是原型模式的精髓。当从零开始创建一个对象非常复杂或耗时的时候,克隆一个现成的模板会高效得多。
二、没有原型模式的代码:昂贵的创建成本
假设我们在开发一款游戏,需要在一个关卡中生成 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); // [ '咆哮' ] ✅ 成功隔离!五、优缺点与适用场景
优点
- 性能卓越:当创建对象的成本很高时(耗时、耗资源),原型模式通过克隆来规避这些成本,性能优势巨大。
- 简化创建:简化了复杂对象的创建过程,因为你可以通过复制一个已配置好的对象来得到新对象。
缺点
- 需要实现
clone方法:每个需要被克隆的类都必须配备一个clone方法,并且需要谨慎地处理深拷贝和浅拷贝的问题。 - 可能破坏封装:
clone方法可能会需要访问对象内部的私有属性,这可能会破坏其封装性。
适用场景
- 当一个对象的创建过程非常耗时或消耗大量资源时。
- 当需要创建大量相似的对象,它们之间只有少量属性不同时。
- 在一个系统中,已经存在一个对象,而你又需要一个跟它状态完全一样的新对象时。