目录
-
- 前言引入原型模式
-
- 头脑风暴
- [传统方式 vs 原型模式](#传统方式 vs 原型模式)
- 实战案例:飞机大战中的分身术
- 原型模式实现的关键秘密
- 原型编程范型的一些规则
-
- 原型编程的四大铁律(门规)
-
- 所有数据都是对象
- [想要新对象?别 new 类了,找个原型克隆一份!](#想要新对象?别 new 类了,找个原型克隆一份!)
- 对象会记得它的"亲爹"是谁(原型)
- 如果对象不会干某件事,它会把任务交给它的"原型爸爸"
- JavaScript中的原型继承
-
- [1️⃣ 所有的数据都是对象(或接近)](#1️⃣ 所有的数据都是对象(或接近))
- [2️⃣ 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它](#2️⃣ 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它)
- [3️⃣ 对象会记住它的原型](#3️⃣ 对象会记住它的原型)
- [4️⃣ 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型](#4️⃣ 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型)
- 结语:
前言引入原型模式
本文的内容深受《JavaScript设计模式》
一书的启发,特别是关于原型模式的讨论,该书深入浅出地介绍了这一重要的设计模式及其在JavaScript
语言中的实现。原型模式不仅是众多设计模式中的一员,它更是构建JavaScript
这门语言基础的核心之一。通过这本书,我们得以从更加简单的Io
语言入手,逐步理解原型模式的概念,并学习如何在JavaScript
中应用这一模式来创建强大而灵活的对象系统。
头脑风暴
想象一下,你是一个程序员界的"Ctrl+C / Ctrl+V"
大师。你不想每次都重新写代码,也不想手动配置一堆参数。你只想------一键克隆,天下我有!这,就是我们今天要说的主角:原型模式(Prototype Pattern
)。
它不只是设计模式,它是一种编程哲学!
原型模式不仅仅是一个设计模式,它还是一个"编程泛型"级别的存在。你可以把它理解为:
"别跟我说什么类、继承、
new
对象那一套,给我一个样板,我能克隆出一整个世界。"
从设计模式的角度来看,原型模式的核心思想是:与其造个类慢慢 new
,不如找个现成的,咔嚓一下,直接复制一份!
传统方式 vs 原型模式
通常我们要创建一个对象,得先定义一个类,然后 new
出来一个实例。比如:
javascript
let plane = new FighterPlane("红色", 100, "高级炮弹", 50);
这看起来很标准,但问题是:如果你想造一个跟这个飞机一模一样的分身呢?血量、武器、防御值、皮肤颜色......都得一个个手动传进去?那不是要命吗?你是写代码的,又不是在填表格!这时候,原型模式就闪亮登场了!原型模式是怎么干的?
原型模式说:
"嘿,别整这些麻烦事了。你不是已经有一个完美的飞机了吗?拿它当模板,克隆一个不就完了?"
于是你就调用了一个方法,比如:
javascript
let clonePlane = originalPlane.clone();
一句话搞定,啥都不用管。原飞机有什么属性,新飞机就自动拥有,连它的"坏脾气"都一起复制过去了!
实战案例:飞机大战中的分身术
假设你在开发一款网页游戏《飞机大战》,某个 boss
飞机突然大喊一声:"我要分身!",你是不是得手忙脚乱地记下它当前的血量、攻击力、装备等级、飞行姿势......然后再 new
出一个一样的?不用了!现在只要一句:
javascript
let 分身 = 真身.clone();
克隆出来的分身不仅长得像,连战斗状态都同步了。真·完美复制!如果使用原型模式,我们只需要调用负责克隆的方法,便能完成同样的功能。
原型模式实现的关键秘密
通过上面讲解你还以为程序员只会 new
对象?不不不,我们还有更高级的操作------复制粘贴对象!
而实现这个魔法的关键,就是看语言有没有提供一个叫 .clone()
的方法。可惜的是,JavaScript
并没有原生的 .clone() 方法
(别哭别沮丧,JS
本来就不是那种贴心暖男型又或者贴心邻家大姐姐语言),但它给了我们另一个工具箱里的神器:Object.create()
,这玩意儿就像是 JavaScript
界的"克隆羊多利",只要你给它一个"母体对象",它就能给你造出一个一模一样的副本!
实战演练:造一架能分身的飞机
让我们来看一段代码,看看怎么用 Object.create() 把飞机克隆出来!
javascript
var Plane = function() {
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
};
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
这段代码干了啥?你可以理解为:我们先 new 出了一个基础版小破飞机,然后给它加了个 buff,血量 +400,攻击力 +9,防御力也猛涨。现在它已经是一架"战神级飞机"了!那问题来了:你想再搞一架一模一样的飞机怎么办?难道要重新 new,再一个个属性设置一遍?太麻烦了!这时候,就该请出我们的主角登场了:💥 克隆大法好!
javascript
var clonePlane = Object.create(plane);
一句话,搞定克隆!就像你拿着这架飞机去复印店说:"老板,来一份复印件,不要改样式,我要原样再来一份。"于是你就得到了一架新的飞机,连它的"战斗状态"都一毛一样:
javascript
console.log(clonePlane); // 输出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}
但是捏,老旧浏览器怎么办?有困难就解决困难!发动手动缝合克隆术!但总有那么几个"古董级浏览器",比如IE8
及以下,它们一脸懵逼地说:"什么?Object.create
是个啥?",那怎么办?别慌,我们可以自己写一个"土法"克隆术:
javascript
Object.create = Object.create || function(obj) {
var F = function() {};
F.prototype = obj;
return new F();
}
这相当于你在对浏览器说:
"既然你不支持克隆技术,那我就自己搭个克隆实验室!用 prototype 搞点遗传工程,照样能造出一模一样的飞机!"
虽然看起来有点土,但效果一样顶呱呱!
克隆是创建对象的手段
通过上面飞机分身复制术,你以为原型模式只是个"复制粘贴工具人"?错!它真正的身份是------宇宙造物主级别的对象工厂!别看它表面是在"克隆",其实它心里想的是:
"我不只是在复制一个对象,我是在帮你创建一个新的世界。"
🤔 克隆只是手段,造对象才是目的!就像你去餐厅点了一份红烧肉,厨师说:"哎呀今天没肉了,我就给你端了一盘一模一样的昨天剩菜。"虽然看起来一样,但本质不一样!
原型模式也是一样:
表面上看:它在"复制"一个对象。
实际上:它是在用"复制"这种方式,来创建一个新对象。
换句话说:克隆只是过程,不是目的。就像洗澡是为了干净,不是为了泡水。
💼 举个 Java
程序员的苦逼例子
在 Java
这种"类型洁癖症晚期"语言中,写代码就像是在做数学证明题:类型必须严格匹配,创建对象要 new
某个具体的类名,如果你想换实现?不好意思,得改代码、加依赖、重新编译......,这时候设计模式就站出来说话了:
"兄弟,咱们得解耦啊!"
"不能直接 new 对象,得搞个工厂出来!"
于是就有了:
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
结果呢?本来只是想造一架飞机,现在还得先建个"飞机制造工厂公司集团有限公司"。更惨的是,每个飞机型号都得配一个对应的工厂......这代码量,简直爆炸!
原型模式:轻装上阵的造物术
这时候原型模式闪亮登场了,它甩掉所有繁琐的类和工厂,只说一句话:
"别整那些虚的,给我一个样板,我能造出一个一模一样的。"
这就像一个小女孩指着商店里的玩具飞机说:
"我要这个!"
而不是说:
"我要一个飞行器,材质塑料,动力系统为螺旋桨驱动,翼展
15cm
......"
她不懂那么多术语,但她知道:这个就是我要的对象!所以,原型模式的本质是:
✅ 不需要知道具体类名
✅ 不需要 new 出来一堆耦合
✅ 只要有一个对象,就能作为模板,轻松造出新的对象!
当然啦,在 JavaScript
的世界里,这一切变得更加丝滑。因为 JS
本身就是基于原型的语言,它不靠"类"来创建对象,而是靠"原型链"来继承属性。你可以理解为:JS
的对象系统,就是用原型模式搭起来的。所以从这个角度讲,在 JavaScript 中使用原型模式,有点像是:
"给猫装胡须,给鱼装鳃,给程序员发咖啡。"
已经自带技能了好吗!不过,如果你真要用原型模式来做业务逻辑上的对象创建,也不是不行。比如:
javascript
let plane = {
blood: 500,
attackLevel: 10,
defenseLevel: 7,
fire: function() {
console.log("发射导弹!");
}
};
let clonePlane = Object.create(plane);
clonePlane.name = "分身一号";
你看,不需要 new Plane()
,也不需要写构造函数,只要有个原型对象,就可以直接克隆出一个新对象。是不是很像魔法?是不是比写一堆 class
和factory
干净多了?
📚 总结一下:原型模式到底图啥?
传统模式 | 原型模式 |
---|---|
new XXX(),依赖具体类 | 克隆已有对象,不关心类名 |
需要工厂类支持 | 一行代码搞定 |
容易耦合 | 更加灵活 |
原型编程范型的一些规则
原型编程的四大门规:不会就问"我爹"
在 JavaScript
的江湖里,有个神秘的门派叫------原型宗(Prototype Sect
)。这个门派不讲"类"
,不搞"继承"
,他们只信奉一个真理:
"不会?没问题,去问你爹。"
这,就是我们今天要说的------原型编程范型的基本规则。
原型编程的四大铁律(门规)
所有数据都是对象
没错,在这里没有"原始类型"这种说法,哪怕是数字、字符串,都被当作"对象小弟"来看待。你可以理解为:在原型宗的地盘上,连数字都想当大哥。
javascript
let x = 5;
x.isAlsoAnObject = true; // 虽然 JS 会临时包装成对象,但意思到了就行 😄
想要新对象?别 new 类了,找个原型克隆一份!
别人创建对象靠 new Plane()
,原型宗靠"复印机"。只要找到一个现成的对象作为"模板",轻轻一按:
"Ctrl+C
,Ctrl+V
,一个一模一样的飞机就出来了。"
javascript
let plane = { blood: 500, attackLevel: 10 };
let clonePlane = Object.create(plane);
一句话搞定,啥都不用写!
对象会记得它的"亲爹"是谁(原型)
每个对象心里都清楚,自己是从哪个原型克隆来的。就像孩子知道自己老爸是谁一样,JS
中的对象也有一条"血缘链"------原型链(Prototype Chain
)你虽然没有显式地声明继承关系,但它默默地记住了它的"原型爸爸"。
如果对象不会干某件事,它会把任务交给它的"原型爸爸"
这是原型宗最核心的一句话:
"我不行?没关系,我爹行!"
比如你想让一个对象执行某个方法,它自己没这个技能,它就会顺着原型链往上找:
自己有没有?没有。
爸爸有没有?有!借来用!
这就叫做:委托机制(Delegation
)
javascript
let dog = {
bark: function() {
console.log("汪汪汪!");
}
};
let buddy = Object.create(dog);
buddy.bark(); // 输出:"汪汪汪!",虽然 buddy 自己没定义这个方法
🧾 总结一下:原型宗四大门规
规则编号 | 内容描述 | 解释 |
---|---|---|
Rule 1 | 所有数据都是对象 | 数字也会装逼说自己是对象 |
Rule 2 | 得到对象靠克隆原型,不是靠 new 类 不写构造函数 | 只管复制粘贴 |
Rule 3 | 对象知道自己的原型是谁 | 孩子知道自己老爸是谁 |
Rule 4 | 请求失败时,委托给原型处理 自己不会? | 去找你爹! |
JavaScript中的原型继承
在 JavaScript
的世界里,没有 Java
那种"类"的概念,它走的是另一条路------原型编程(Prototype-based Programming
)。这就像:别人靠"父母"生孩子,JS
靠"克隆"造对象。现在我们就来看看 JavaScript
是如何通过原型链实现对象之间的继承关系的。
1️⃣ 所有的数据都是对象(或接近)
JavaScript
在设计之初模仿了 Java
,引入了两套类型机制:基本类型
和对象类型
。基本类型包括 undefined
、number
、boolean
、string
、function
和 object
。但说实话,这种设计有点迷糊。按照 JS
设计者的初衷,除了 undefined
外,其他一切都是对象。为了让 number
、boolean
、string
这些基本类型也能像对象一样被处理,JS
引入了"包装类"。虽然不能说所有数据都是对象,但可以说绝大部分数据是对象。而且,在 JavaScript
中有一个根对象存在,那就是 Object.prototype
。它是所有对象的老祖宗,所有对象追根溯源都来源于这个根对象。
比如下面的例子:
javascript
var obj1 = new Object();
var obj/XMLSchema = {};
console.log(Object.getPrototypeOf(obj1) === Object.prototype); // 输出:true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); // 输出:true
这就像是所有的对象都认同一个老祖宗------Object.prototype
。
2️⃣ 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
在 JavaScript
中,我们并不需要关心克隆一个对象的这些细节,因为这是引擎内部的事儿。当我们调用 new Object()
或者 {}
来创建对象时,引擎会从 Object.prototype
上面克隆出一个新的对象。再来看个例子:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
var a = new Person('sven');
console.log(a.name); // 输出:sven
console.log(a.getName()); // 输出:sven
console.log(Object.getPrototypeOf(a) === Person.prototype); // 输出:true
x
虽然我们用了 new
关键字,但其实并没有真正意义上的"类",Person
只是个构造器。使用 new
创建对象的过程,实际上也只是先克隆 Object.prototype
对象,再进行一些额外操作。
3️⃣ 对象会记住它的原型
每个对象都记得自己的"亲爹"是谁(即它的原型)。为了实现这一点,JavaScript
给对象提供了一个隐藏属性------__proto__
。这个属性指向对象的构造器的原型对象 {Constructor}.prototype
。
例如:
javascript
var a = new Object();
console.log(a.__proto__ === Object.prototype); // 输出:true
这就像是对象之间有一条看不见的线,把它们串在一起。当我们用new
创建对象时,需要手动设置 obj.__proto__ = Constructor.prototype
,这样才能让对象正确地找到它的"亲爹"。
4️⃣ 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型
这条规则就是原型继承的精髓所在。当一个对象无法响应某个请求时,它会顺着原型链把请求传递下去,直到遇到一个能处理该请求的对象为止。
举个例子:
javascript
var obj = { name: 'sven' };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log(a.name); // 输出:sven
在这个过程中,如果对象 a
没有找到 name
属性,它就会沿着原型链向上查找,直到在 obj
中找到了name
属性,并返回其值。再看一个稍微复杂一点的例子:
javascript
var A = function(){};
A.prototype = { name: 'sven' };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log(b.name); // 输出:sven
当尝试访问 b
的 name
属性时,如果 b
自己没有这个属性,它会沿着原型链向上查找,直到在 A.prototype
中找到 name
属性,并返回其值。
再看这段代码执行的时候,引擎做了什么事情:
- 首先,尝试遍历对象
b
中的所有属性,但没有找到name
这个属性。- 查找 name 属性的请求被委托给对象 b 的构造器的原型,它被
b.__proto__
记录着并且指向
B.prototype,而 B.prototype 被设置为一个通过 new A()创建出来的对象。- 在该对象中依然没有找到 name 属性,于是请求被继续委托给这个对象构造器的原型A.prototype。
- 在 A.prototype 中找到了 name 属性,并返回它的值。
和把 B.prototype
直接指向一个字面量对象相比,通过 B.prototype = new A()
形成的原型链比之前多了一层。但二者之间没有本质上的区别,都是将对象构造器的原型指向另外一个对象,继承总是发生在对象和对象之间。
最后还要留意一点,原型链并不是无限长的。现在我们尝试访问对象 a
的 address
属性。而对象 b
和它构造器的原型上都没有 address
属性,那么这个请求会被最终传递到哪里呢?
实际上,当请求达到 A.prototype
,并且在 A.prototype
中也没有找到 address
属性的时候,请求会被传递给 A.prototype
的构造器原型 Object.prototype
,显然 Object.prototype
中也没有address
属性,但 Object.prototype
的原型是 null
,说明这时候原型链的后面已经没有别的节点了。所以该次请求就到此打住,a.address
返回 undefined
。
javascript
a.address // 输出:undefined
🧩 总结一下:原型继承的核心思想
规则编号 | 内容描述 | 理解 |
---|---|---|
Rule 1 | 所有数据都是对象(或接近) | 老祖宗是 Object.prototype |
Rule 2 | 得到对象靠克隆原型,不是靠 new 类 | new 类 不写构造函数,只管复制粘贴 |
Rule 3 | 对象知道自己的原型是谁 | 孩子知道自己老爸是谁 |
Rule 4 | 请求失败时,委托给原型处理 | 自己不会?去找你爹 |
💬 最后一句灵魂总结:
在 JavaScript 的世界里,你不靠"类"吃饭,你靠"原型"混江湖。遇事不懂?先问问你爹再说!
结语:
本文的内容深受《JavaScript设计模式》一书的启发,特别是关于原型模式的讨论,该书深入浅出地介绍了这一重要的设计模式及其在JavaScript语言中的实现。原型模式不仅是众多设计模式中的一员,它更是构建JavaScript这门语言基础的核心之一。通过这本书,我们得以从更加简单的Io语言入手,逐步理解原型模式的概念,并学习如何在JavaScript中应用这一模式来创建强大而灵活的对象系统。
在此,我对《JavaScript设计模式》的作者表示深深的感谢和敬意。是你细致入微的讲解让复杂的设计模式变得易于理解,也为像我这样的开发者提供了宝贵的指导和灵感。如果你希望深入了解JavaScript中的设计模式及其背后的原理,《JavaScript设计模式》绝对是一本不容错过的好书!
致敬------ 《JavaScript设计模式》· 曾探