JavaScript 原型与继承机制详解:从基础概念到实战应用

作者:木棉

时间:2026-10-26

浏览:162

本文深入解析JavaScript的核心机制——原型与继承。通过John和Jane的对象示例,阐明原型链的工作原理及属性覆盖规则。文章指出__proto__的非标准性,并详细演示如何利用构造函数和prototype对象实现类式继承,包括二级原型链的构建方法,帮助开发者掌握JS面向对象编程的本质。

文章目录

原型继承基础概念

这是一篇关于JavaScript面向对象与继承的文章,撰写于1年前。作者循序渐进地讲解,对希望学习JavaScript面向对象编程的同学极具帮助,故尝试翻译。原文链接为Objects and Inheritance in Javascript。 尽管部分JavaScript用户可能无需了解原型或面向对象语言的特性,但来自传统面向对象语言的开发者在使用时会发现JavaScript的继承模型颇为奇特。不同的JS框架提供了各自编写类面向对象(class-like)代码的方法,这使得JavaScript的面向对象特性更加难以理解。由此带来的结果是: 1、缺乏实现面向对象的标准方法。 2、JavaScript面向对象的底层概念未获广泛知晓。 原型继承 原型继承是一个极其简单的概念,其本质如下: 1、若对象a继承自对象b,则称b为a的原型(prototype)。 2、a继承b的所有属性,即若b.x的值为1,则a.x的值也为1。 3、a自身的属性会覆盖b中同名的属性。 让我们通过具体代码观察效果,假设存在一个John Smith和一个继承自他的Jane。

示例代码展示如下:

 
var john = {firstName: 'John', lastName: 'Smith'};
var jane = {firstName: 'Jane'};
jane.__proto__ = john;

此时,我们称john是jane的原型(prototype),jane继承了john的所有属性。


相关代码验证如下:

 
jane.lastName
"Smith" // 该属性继承自john

需要注意的是,对象自身的属性具有更高优先级,具体表现如下:


代码示例如下:

 
jane.firstName; // 该属性覆写了john中的firstName属性
"Jane"

若在之后向john添加新属性,jane也将动态继承该属性,示例如下:


代码演示:

 
john.hair = 'brown'; // 给john添加一个新属性
jane.hair;
"brown" // 结果表明jane继承了新添加的属性

假设jane结婚后获得了新的姓氏(lastName),操作如下:


代码实现:

 
jane.lastName = 'Doe'

该属性覆盖了john中同名的属性(lastName)。


验证代码如下:

 
jane.lastName
"Doe"

若随后删除jane的lastName属性,


代码操作:

 
delete jane.lastName
该属性的值将恢复为john中的值。
[code]
jane.lastName
"Smith"

此时,jane还可以继承自其他对象。在此链中可存在任意多个继承层级,我们称之为原型链(prototype chain)。实际上,john自身也存在一个prototype属性。


代码查看:

 
john.__proto__;
Object { }

在Firebug控制台中,john.__proto__的值显示为Object{},但这代表的是Object.prototype对象——即所有对象的父类。



以上即为对原型继承的简要描述,看起来逻辑清晰,对吗?


然而实际上,我们通常不应直接使用__proto__属性。



这里有一个需要注意的情况:


IE浏览器不支持__proto__属性。实际上,__proto__并非ECMAScript规范中的标准属性,且Mozilla计划在未来的Firefox版本中移除该属性。



但这并不意味着__proto__属性不存在。尽管在某些浏览器中无法直接访问__proto__,但它仍以某种形式存在,我们仍需与之交互,只是访问方式不再那么直接。



类和继承

构造函数与原型链机制

因此,我们可以得出结论:JavaScript中没有“类”的概念。


请牢记:JavaScript中没有类。


那么,方法和继承又是如何实现的?


答案是通过原型(prototype)。在传统面向对象语言中,方法依赖于类;而在JavaScript中,方法依赖于对象的原型,且原型与对象的构造函数(constructor)绑定在一起。



在JavaScript中,函数充当构造器(constructor)的角色。通过new运算符,你可以将函数作为构造函数使用。以下代码展示了创建Cat函数的过程:


代码定义如下:

 
function Cat(name){ // <-这是一个常规的函数
this.name = name // this指向新建的对象
} 

上述代码会自动创建一个Cat.prototype对象。


查看该对象:

 
Cat.prototype
Cat { }

我们可以使用new操作符来创建Cat的一个实例。


代码示例:

 
var garfield = new Cat('Garfield') // 创建一个实例 - Cat函数充当了构造函数

此时,Cat.prototype对象成为所有通过new Cat()创建的对象的原型,例如:


代码验证:

 
garfield.__proto__ === Cat.prototype
true // 可以看到,`Cat.prototype` 现在是garfield对象的原型

接下来,我们向Cat.prototype添加一个方法,添加后该方法可被garfield访问。


代码实现:

 
Cat.prototype.greet = function(){
console.log('Meow, I am ' + this.name)
} 
garfield.greet()
"Meow, I am Garfield"

其他Cat的实例也能访问此方法。


代码示例:

 
var felix = new Cat('Felix')
felix.greet()
"Meow, I am Felix"

由此可见,在JavaScript中,方法是依赖于对象的原型(prototype)存在的。



实际上,我们也可以为garfield直接添加方法,它会覆写Cat.prototype中的同名方法,如下所示:


代码演示:

 
garfield.greet = function(){
console.log("What's new?");
}; 
garfield.greet();
"What's new?"

但这不会影响其他对象。


代码验证:

 
felix.greet();
"Meow, I am Felix"

因此,在JavaScript中,方法可以直接与对象关联,也可以与对象的原型关联,还可以与对象的任意父辈对象(即原型链中的任意一环)关联。继承正是通过这种方式实现的。



构建二级原型链

若要创建二级原型链(prototype chain),我们需要先创建另一个构造函数,例如Animal。


代码定义:

 
function Animal(){
} 

接下来,我们需要将Cat.prototype的原型指向一个Animal的对象,这样Cat的实例就能继承所有Animal的方法。因此,我们将Cat.prototype的值设置为Animal的实例,如下所示:


代码实现:

 
Cat.prototype = new Animal();

此外,我们还需告知新的Cat.prototype,它实际上是Cat的一个实例:


代码设置:

 
Cat.prototype.constructor = Cat // 让`Cat.prototype` 知道它是Cat的一个实例

虽然这样做的主要目的是明确类之间的层次关系,但通常建议这样做。



现在,既然继承自Animal.prototype和Cat.prototype的对象属于动物类,那么所有Cat的实例也间接继承自Animal.prototype。如果我们给Animal.prototype添加一个新方法,所有的Cat实例也能访问该方法。


代码示例:

 
Animal.prototype.breed = function(){
console.log('Making a new animal!');
return new this.constructor();
}; 
var kitty = garfield.breed();
Making a new animal!

通过上述代码,我们实现了继承,过程相对简单。



结语

尽管JavaScript中基于原型的继承看似怪异且需要时间适应,但其核心思想非常简洁。只要真正理解这些本质概念,你便有信心驾驭那些质量参差不齐的JavaScript面向对象代码。(完)^_^

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章