5.16 原型继承
事实上,原型继承的内部执行方式是相当复杂的。如果只是希望能够使用原型继承,而没有弄清其用法的话,反而可能会导致混乱的局面。因此,首先仅说明一下其形式。按代码清单 5.9 中的类定义为模板,并以原型继承的方式改写,就能得到代码清单 5.11。
// 代码清单 5.9 // 相当于进行类定义 function hzhClass(x, y) { this.x = x; this.y = y; } hzhClass.prototype.show = function() { console.log(this.x, this.y); } // 代码清单 5.11 的构造函数调用(实例生成) var hzh = new hzhClass(3, 2); // 方法调用 console.log("调用hzh实例的show方法:"); hzh.show();
[Running] node "e:\HMV\JavaScript\JavaScript.js" 调用hzh实例的show方法: 3 2 [Done] exited with code=0 in 0.199 seconds
代码清单 5.9 与代码清单 5.11 的区别在于,前者的方法定义直接是对象实例的属性,而后者不是。在代码清单 5.11 中,方法 show 并不是对象 obj 的直接属性,但也可以被调用。从表面上来看,它是从另一个对象(hzhClass.prototype)的属性继承而来的。这就是对原型继承的一种形式上的理解。
在 JavaScript 中,保存有值的属性和保存有函数的属性之间并没有什么特别的区别,所以除了方法之外其他的值也能够被原型继承。不过在实际中需要进行原型继承的大多是方法。此外,将构造函数名与类名进行替换不会造成什么问题,所以形式上像下面这样使用原型继承即可。
// 对原型继承形式上的理解 类名.prototype.方法名 = function(方法的参数) { 方法体 }
5.16.1 原型链
原型继承支持一种称为原型链的功能。使用原型链有两个前提。
- 所有的函数(对象)都具有名为 prototype 的属性(prototype 属性所引用的对象则称为prototype 对象)。
- 所有的对象都含有一个(隐藏的)链接,用以指向在对象生成过程中所使用的构造函数(Function 对象) 的 prototype 对象。
在 ECMAScript 的标准中,prototype 属性被称为 explicit prototype property,而隐藏的链接被称为 implicit prototype link。前者称为“prototype 引用”,而将后者称为“隐式链接”。
在满足了以上前提的情况下,原型链将以以下方式运行。
对象对属性的读取(以及对方法的调用)是按照以下顺序查找的。
- 对象自身的属性。
- 隐式链接所引用的对象(即构造函数的 prototype 对象)的属性。
- 第 2 项中的对象的隐式链接所引用的对象的属性。
- 反复按第 3 项的规则查找直至全部查找完毕(查找的终点是 Object.prototype 对象)。
如果不考虑原型链这一术语的话,会发现其本质其实就是对隐式链接的属性继承。由于隐式链接所引用的对象是构造函数的 prototype 对象,因此事实上这就是在前面小节中所说的“类名.prototype.方法名”的继承方式。此外需要注意,由对象字面量生成的对象的隐式链接引用的是 Object.prototype。
而在写入对象的属性的时候,则是按照以下顺序进行属性查找的。这时的属性改写中不会发生继承。
- 对象自身的属性
请注意,在读取和写入的时候,继承的执行方式是不同的,不过这种不对称性其实也是理所当然的。根据原型链的原理,所有的对象最终都会具有一个引用了 Object.prototype 对象的隐式链接。如果对属性的改写会影响到上一级对象的话,那么即使仅仅是改写了某一个对象的 toString 方法,也会对其他所有对象造成影响。这样一来就很难控制程序了。
另一方面,由于在读取中会发生继承,所以若是改写了某个隐式链接的 toString 方法,就能够在原型继承了该对象的对象中使用这一新的实现。这种对实现的继承或者说是对操作的继承的做法,正是对面向对象技术的一种灵活运用。
下面要介绍的术语可能会造成一些理解上的混乱:“隐式链接”所引用的对象被称为原型对象(请参见专栏)。在接受了这个术语之后,对于原型继承的说明就变得非常简单了。只需要通过“在读取属性时,对属性对象的属性进行继承”这样一句话就可以完成定义。
下面是对原型链的原理的图示(图 5.9)。请注意,变量和引用的对象是分开写的。这里要再次提醒的是,对象自身是没有名字的。
图 5.9 原型链的原理
5.16.2 原型链的具体示例
接下来,将说明原型链的具体示例以及其内部的执行方式。首先请看代码清单 5.10。
代码清单 5.10 原型链的具体示例(属性读取)
function hzhClass() { this.x = '黄子涵的x在hzhClass中'; } var hzh = new hzhClass(); // 通过 hzhClass 构造函数生成对象 console.log("访问对象hzh的属性x:"); console.log(hzh.x); console.log(""); console.log("测试一下hzh中没有属性z:"); console.log(hzh.z); console.log(""); // Function 对象具有一个隐式地 prototype 属性 // 在构造函数 prototype 对象新增属性 z hzhClass.prototype.z = "黄子涵的z在hzhClass.prototype"; console.log("构造函数prototype对象的z属性:"); console.log(hzh.z);
[Running] node "e:\HMV\JavaScript\JavaScript.js" 访问对象hzh的属性x: 黄子涵的x在hzhClass中 测试一下hzh中没有属性z: undefined 构造函数prototype对象的z属性: 黄子涵的z在hzhClass.prototype [Done] exited with code=0 in 0.223 seconds
在读取对象 hzh 的属性的时候,将首先查找自身的属性。如果没有找到,则会进一步查找对象 hzhClass 的 prototype 对象的属性。这就是原型链的基本原理。这样一来,在通过 hzhClass 构造函数生成的对象之间就实现了对 hzhClass.prototype 对象的属性的共享。
这种共享用面向对象的术语来说就是继承。通过继承可以生成具有同样执行方式的对象。不过请注意,在上面的代码中,如果修改 hzhClass.prototype,已经生成的对象也会发生相应的变化。
而属性的写入与删除则与原型链无关。请看代码清单 5.11、代码清单 5.12 和代码清单 5.13。
代码清单 5.11 原型链的具体示例(属性写入)
function hzhClass () { this.x = 'x在hzhClass中'; } hzhClass.prototype.y = '黄子涵的y在hzhClass.prototype中'; var hzh = new hzhClass(); // 通过 hzhClass 构造函数来生成对象 console.log("读取hzh对象的y属性:"); console.log(hzh.y); // 通过原型链读取属性 console.log(""); hzh.y = '黄子涵'; // 在对象 hzh 中新增直接属性 y console.log("读取hzh对象的y属性:"); console.log(hzh.y); // 读取直接属性 console.log(""); var hzh2 = new hzhClass(); console.log("读取hzh2对象的y属性:"); console.log(hzh2.y); // 在其他的对象中,属性 y 不会发生变化
[Running] node "e:\HMV\JavaScript\JavaScript.js" 读取hzh对象的y属性: 黄子涵的y在hzhClass.prototype中 读取hzh对象的y属性: 黄子涵 读取hzh2对象的y属性: 黄子涵的y在hzhClass.prototype中 [Done] exited with code=0 in 0.184 seconds
代码清单 5.12 原型链的具体示例(属性删除)
function hzhClass () { this.x = 'x在hzhClass中'; } hzhClass.prototype.y = '黄子涵的y在hzhClass.prototype中'; var hzh = new hzhClass(); // 通过 hzhClass 构造函数来生成对象 console.log("读取hzh对象的y属性:"); console.log(hzh.y); // 通过原型链读取属性 console.log(""); hzh.y = '黄子涵'; // 在对象 hzh 中新增直接属性 y console.log("读取hzh对象的y属性:"); console.log(hzh.y); // 读取直接属性 console.log(""); var hzh2 = new hzhClass(); console.log("读取hzh2对象的y属性:"); console.log(hzh2.y); // 在其他的对象中,属性 y 不会发生变化 console.log(""); delete hzh.y; // 删除属性 y console.log("这里是原型链中的y属性:"); console.log(hzh.y); // 该直接属性不存在,因此将搜索原型链 console.log(""); console.log("测试一下有没有删除hzh对象的y属性:"); console.log(delete hzh.y); // 虽然 delete 运算的值为 true...... console.log(""); console.log("测试一下原型链中的y属性:"); console.log(hzh.y); // 但无法 delete 原型链中的属性
[Running] node "e:\HMV\JavaScript\JavaScript.js" 读取hzh对象的y属性: 黄子涵的y在hzhClass.prototype中 读取hzh对象的y属性: 黄子涵 读取hzh2对象的y属性: 黄子涵的y在hzhClass.prototype中 这里是原型链中的y属性: 黄子涵的y在hzhClass.prototype中 测试一下有没有删除hzh对象的y属性: true 测试一下原型链中的y属性: 黄子涵的y在hzhClass.prototype中 [Done] exited with code=0 in 0.171 seconds
图5.13 属性写入和属性删除的执行方式
这里hzhClass等价于MyClass
5.16.3 原型继承与类
hzhClass等价于MyClass
在代码清单 5.10 中,如果没能找到 MyClass.prototype 对象的属性的话,则会继续搜索原型链。
之后将会在生成了 MyClass.prototype 对象的构造函数的 prototype 对象的属性中搜索。在默认情况下,MyClass.prototype 对象的构造函数是一个 Object 对象。因此,将会查找 Object.prototype 对象的属性。可以通过向 Object.prototype 对象增加新的属性来验证这一操作。不过如果在实际的代码中修改 Object.prototype 的话,将会对程序造成巨大的影响,所以并不推荐这么做。toString 方法是原本就存在于 Object.prototype 对象中的一个属性,可以通过对它调用来验证上面的说法(代码清单 5.14)。可以通过 hasOwnProperty 方法来确认一个属性是否直接属于某个对象。需要注意的是,toString 并不是 Object 对象所具有的属性,而是 Object.prototype 对象的属性。
代码清单 5.14 对是否存在一个指向 Object.prototype 的隐式链接的确认
function hzhClass() { this.x = '黄子涵的x在hzhClass中'; } var hzh = new hzhClass(); // 通过 hzhClass 构造函数生成对象 console.log("访问对象hzh的属性x:"); console.log(hzh.x); console.log(""); console.log("测试一下hzh中没有属性z:"); console.log(hzh.z); console.log(""); // Function 对象具有一个隐式地 prototype 属性 // 在构造函数 prototype 对象新增属性 z hzhClass.prototype.z = "黄子涵的z在hzhClass.prototype"; console.log("构造函数prototype对象的z属性:"); console.log(hzh.z); console.log(""); console.log("检验是否可以对对象hzh调用toString方法:"); console.log(hzh.toString()); console.log(""); console.log("检验对象hzh中是否存在toString方法:"); console.log(hzh.hasOwnProperty('toString')); console.log(""); console.log("检验Object.prototype对象中是否存在toString方法:"); console.log(Object.prototype.hasOwnProperty('toString')); console.log(""); console.log("检验Object对象中是否是否存在toString方法:"); console.log(Object.hasOwnProperty('toString')); // 注意,在 Object 中不存在 toString 方法
[Running] node "e:\HMV\JavaScript\JavaScript.js" 访问对象hzh的属性x: 黄子涵的x在hzhClass中 测试一下hzh中没有属性z: undefined 构造函数prototype对象的z属性: 黄子涵的z在hzhClass.prototype 检验是否可以对对象hzh调用toString方法: [object Object] 检验对象hzh中是否存在toString方法: false 检验Object.prototype对象中是否存在toString方法: true 检验Object对象中是否是否存在toString方法: false [Done] exited with code=0 in 0.182 seconds
借助原型继承,在 JavaScript 中实现了类似于 Java 与 C++ 等基于类的程序设计语言中的类型层级的机制。
5.16.4 对于原型链的常见误解以及 _proto_
属性
对于原型链,有以下常见的错误理解。
- 在搜索了自身的属性之后,将会查找构造函数自身的属性(对于代码清单 5.10 中的例子来说,指的就是查找 hzhClass.z)。
- 在搜索了自身的属性之后,将查找对象的 prototype 对象的属性(对于代码清单 5.10 中的例子来说,指的就是查找 obj.prototype.z)。
原型链最终是通过“隐式链接”连接而成的。在一些 JavaScript 实现中具有
_proto_
这样一个属性,它指向了隐式链接所引用的对象。
不过在 ECMAScript 的标准中并没有
_proto_
属性,所以是否可以使用该属性还要取决于具体的实现。
5.16.5 原型对象
对象的隐式链接(
_proto_
属性)所引用的对象称为原型对象。下面的代码将说明这种命名方式可能会引起的问题。
function hzhClass() {} var hzh = new hzhClass();
hzhClass.prototype 与
obj._proto_
引用了同一个对象,即对象 hzh 的原型对象。不过容易弄错的一点是,hzhClass.prototype的引用对象并不是 hzhClass 的原型对象。(那么 hzhClass 对象的原型对象是什么呢?答案是 Function.prototype 所引用的对象。)。
5.16.6 ECMAScript 第 5 版与原型对象
原型链之所以不容易被理解,其重要原因之一是由于隐式链接的存在。在之前的小节中所介绍的
_proto_
属性是非通用的增强功能,因此,实际上并没有一种可以从一个对象追溯至其原型对象的官方方法。这也是为什么隐式链接会被称为隐式链接。
这种情况在 ECMAScript 第 5 版中得到了改善。在 ECMAScript 第 5 版中有 getPrototypeOf 这样一个方法,它将会返回“隐式链接”所引用的对象。也就是说,在官方的标准中出现了一个和
_proto_
属性这一非通用功能的作用相同的方法。代码清单 5.12 介绍了从一个对象中直接获取其原型对象的具体方法。
代码清单 5.12 获取原型对象的三种方法
// 前提条件 function hzhClass() {} var Proto = hzhClass.prototype; var hzh = new hzhClass(); // 对象 hzh 的原型对象是对象 proto // 通过对象实例取得(ES5中最直接的方法) var hzhProto1 = Object.getPrototypeOf(hzh); console.log("通过getPrototypeOf方法取得:"); console.log(hzhProto1); console.log(""); // 通过对象实例取得(使用_proto_属性这一增强功能) var hzhProto2 = hzh._proto_; console.log("通过_proto_属性取得:"); console.log(hzhProto2); console.log(""); // 通过对象实例以及其构造函数取得(无法确保总是有效) var hzhProto3 = hzh.constructor.prototype; console.log("通过构造函数取得:"); console.log(hzhProto3);
[Running] node "e:\HMV\JavaScript\JavaScript.js" 通过getPrototypeOf方法取得: hzhClass {} 通过_proto_属性取得: undefined 通过构造函数取得: hzhClass {} [Done] exited with code=0 in 0.409 seconds
虽然
_proto_
属性是一种非通用的功能,不过由于它更为直观且易于理解,因此接下来,我们还是以它为基础进行说明。如果一定要遵循标准的话,只需将使用了_proto_
的代码部分替换为 Object.getPrototypeOf 即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?