深入理解JavaScript设计模式之原型模式

目录

前言引入原型模式

本文的内容深受《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(),也不需要写构造函数,只要有个原型对象,就可以直接克隆出一个新对象。是不是很像魔法?是不是比写一堆 classfactory 干净多了?

📚 总结一下:原型模式到底图啥?

传统模式 原型模式
new XXX(),依赖具体类 克隆已有对象,不关心类名
需要工厂类支持 一行代码搞定
容易耦合 更加灵活

原型编程范型的一些规则

原型编程的四大门规:不会就问"我爹"

JavaScript 的江湖里,有个神秘的门派叫------原型宗(Prototype Sect)。这个门派不讲"类",不搞"继承",他们只信奉一个真理:

"不会?没问题,去问你爹。"

这,就是我们今天要说的------原型编程范型的基本规则。

原型编程的四大铁律(门规)

所有数据都是对象

没错,在这里没有"原始类型"这种说法,哪怕是数字、字符串,都被当作"对象小弟"来看待。你可以理解为:在原型宗的地盘上,连数字都想当大哥。

javascript 复制代码
let x = 5;
x.isAlsoAnObject = true; // 虽然 JS 会临时包装成对象,但意思到了就行 😄
想要新对象?别 new 类了,找个原型克隆一份!

别人创建对象靠 new Plane(),原型宗靠"复印机"。只要找到一个现成的对象作为"模板",轻轻一按:
"Ctrl+CCtrl+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,引入了两套类型机制:基本类型对象类型。基本类型包括 undefinednumberbooleanstringfunctionobject。但说实话,这种设计有点迷糊。按照 JS 设计者的初衷,除了 undefined 外,其他一切都是对象。为了让 numberbooleanstring 这些基本类型也能像对象一样被处理,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 

当尝试访问 bname 属性时,如果 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()形成的原型链比之前多了一层。但二者之间没有本质上的区别,都是将对象构造器的原型指向另外一个对象,继承总是发生在对象和对象之间。

最后还要留意一点,原型链并不是无限长的。现在我们尝试访问对象 aaddress 属性。而对象 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设计模式》· 曾探

相关推荐
qqxhb10 分钟前
零基础设计模式——结构型模式 - 组合模式
设计模式·组合模式
孤独得猿11 分钟前
类的设计模式——单例、工厂以及建造者模式
单例模式·设计模式·建造者模式
agenIT20 分钟前
vue3 getcurrentinstance 用法
javascript·vue.js·ecmascript
码农捻旧31 分钟前
JavaScript 性能优化按层次逐步分析
开发语言·前端·javascript·性能优化
我是哈哈hh44 分钟前
【Vue3】生命周期 & hook函数 & toRef
开发语言·前端·javascript·vue.js·前端框架·生命周期·proxy模式
傻虎贼头贼脑1 小时前
day28JS+Node-JS打包工具Webpack
开发语言·前端·javascript·webpack
菥菥爱嘻嘻2 小时前
React---day2
前端·javascript·react.js
软件技术NINI2 小时前
html css js网页制作成品——HTML+CSS+js醇香咖啡屋网页设计(5页)附源码
javascript·css·html
coding随想3 小时前
多语言视角下的 DOM 操作:从 JavaScript 到 Python、Java 与 C#
java·javascript·python
杰克逊的日记4 小时前
java中的设计模式
java·开发语言·设计模式