JavaScript 原型与原型链深度解析及继承模式详解
本文深入解析JavaScript中的原型与原型链机制,阐明构造函数、原型对象与实例间的指针关系。详细探讨原型链继承的局限性,并对比介绍组合继承与寄生组合式继承的实现原理及代码优化,同时手写实现instanceof运算符以验证原型链查找逻辑。
本文详细解析了JavaScript中的原型与原型链机制,旨在为开发者提供清晰的参考与深入理解。
前言
在JavaScript开发中,原型是一个高频出现的概念。初学者常误以为它仅指代用于继承的某个对象,这种理解并不全面。本文将深入剖析原型与原型链的内部细节,并在此基础上探讨JavaScript中多种继承方式的实现原理。
原型
在探讨原型之前,需先回顾JavaScript中的对象概念。在JS中,万物皆对象,例如字符串、数值、布尔、数组等。根据ECMA-262规范,对象被定义为无序属性的集合,这些属性可以包含基本值、其他对象或函数。对象是拥有属性和方法的数据载体,为了更精准地描述这些事物及其行为,便引入了原型的概念。
无论何时,只要创建了一个新函数,JavaScript引擎就会根据特定规则为该函数创建一个prototype属性,该属性指向该函数的原型对象。同时,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。
这段描述源自《JavaScript高级程序设计》,概念清晰易懂。以下通过创建实例的代码示例来具体说明。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
};
}
const person1 = new Person("gali", 18);
const person2 = new Person("pig", 20);

在上述示例中,person1和person2均为构造函数Person()的实例。Person.prototype指向Person函数的原型对象,而Person.prototype.constructor则指向Person函数本身。每个Person实例内部都包含一个内部属性__proto__,该属性指向Person.prototype,正如示意图所示,从而确立了如下关系。
console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
继承
JavaScript是一种基于原型的语言,这与基于类的面向对象语言存在显著差异。在JS中不存在“类”的概念,取而代之的是原型对象。原型对象充当模板角色,新创建的对象可以从原型对象中获取属性。那么,JavaScript具体是如何实现继承的呢?
在深入讨论继承之前,首先需要理解原型链这一核心概念。
原型链
构造函数、原型和实例之间的关系已十分明确:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例对象则包含一个指向原型对象的指针。这种关系直观易懂。但如果我们希望让一个原型对象等于另一个类型的实例对象,情况会如何变化?这将衍生出相同的关系结构,此时该原型对象将包含一个指向另一个原型对象的指针,而另一个原型对象又包含一个指向另一个构造函数的指针。若这个另一个原型对象又是另一个类型的实例,便形成了原型链。文字描述可能略显抽象,下面通过代码示例进行说明。
function SuperType() {
this.name = "张三";
}
SuperType.prototype.getSuperName = function() {
return this.name;
};
function SubType() {
this.subname = "李四";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubName = function() {
return this.subname;
};
const instance = new SubType();
console.log(instance.getSuperName()); // 张三
在上述示例中,SubType的原型对象被设置为SuperType构造函数的实例。此时,SubType的原型对象拥有一个__proto__属性,该属性指向SuperType的原型对象。由于instance是SubType的实例,它必然能共享SubType原型对象的属性;又因为SubType的原型对象指向SuperType原型对象的属性,因此可以得出结论:instance继承了SuperType原型的所有属性。
众所周知,所有函数的默认原型都是Object的实例,因此可以推断,SuperType的默认原型必然包含一个__proto__属性,该属性指向Object.prototype。
图中由__proto__属性连接而成的链条即为原型链,其终点为null。

上图清晰地展示了原型链的结构。这让人联想到JavaScript中的一个运算符:instanceof,它用于判断一个实例对象是否属于某个构造函数。
A instanceof B; // true
其实现原理是在A的原型链上查找是否存在原型等于B.prototype。如果一直追溯到A原型链的顶端null仍未找到等于B.prototype的原型,则返回false。下面手写实现一个instanceof函数,这也是许多大厂面试中常见的手写题。
function Instance(left, right) {
left = left.__proto__;
right = right.prototype;
while (true) {
if (left === null) return false;
if (left === right) return true;
// 继续在left的原型链向上找
left = left.__propo__;
}
}
原型链继承
在上述示例中,instance通过原型链继承了SuperType原型的属性,其继承原理正是基于原型链。原型链功能强大,可用于实现继承。然而,单纯的原型链继承存在明显的缺陷。
- 实例属性会变为原型属性,从而影响其他实例
- 在创建子类型实例时,无法向超类型的构造函数传递参数
function SuperType() {
this.colorArr = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType();
const instance1 = new SubType();
instance1.colorArr.push("black");
console.log(instance1.colorArr); // ["red", "blue", "green", "black"]
const instance2 = new SubType();
console.log(instance2.colorArr); // ["red", "blue", "green", "black"]
当SubType的原型被设置为SuperType的实例时,SubType的实例对象通过原型链继承到colorArr属性。此时,若修改其中一个实例对象从原型链中继承到的原型属性,便会影响到其他实例。例如,对instance1.colorArr的修改,会直接反映在instance2.colorArr中。
组合继承
组合继承是指组合原型链和构造函数的技术,它通过原型链实现对原型属性和方法的继承,同时通过借用构造函数实现对实例属性的继承。
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
// 继承属性,借用构造函数实现对实例属性的继承
SuperType.call(this, name);
this.age = age;
}
// 继承原型属性及方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
};
const instance1 = new SubType("gali", 18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // gali
instance1.sayAge(); // 18
const instance2 = new SubType("pig", 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // pig
instance2.sayAge(); // 20
在上述示例中,通过借用构造函数继承实例属性,通过原型继承原型属性与方法。这样既能让不同的实例拥有各自的属性,又能共享相同的方法,避免了原型链继承中实例属性修改影响其他实例的问题。组合继承是JavaScript中最常用的继承方式。
寄生组合式继承
尽管组合继承是最常用的继承方式,但观察上述示例可以发现,组合继承中调用了两次SuperType函数。回顾一下,在第一次调用SubType时。
SubType.prototype = new SuperType();
调用结束后,SubType.prototype会从SuperType继承到两个属性:name和colors。这两个属性存在于SubType的原型中。而在第二次调用时,即在创建实例对象时调用SubType构造函数,这会再次调用SuperType构造函数。
SuperType.call(this, name);
第二次调用后,新的实例对象上会创建实例属性:name和colors。这意味着,此时实例对象和原型对象拥有两个同名属性。这种做法不仅浪费内存,效率也较低。
为解决此问题,引入了寄生组合继承方式。其核心在于,无需为了定义SubType的原型而调用SuperType构造函数,此时只需获取SuperType原型的一个副本,并将其赋值给SubType的原型即可。
function InheritPrototype(subType, superType) {
// 创建超类型原型的一个副本
const prototype = Object(superType.prototype);
// 添加constructor属性,因为重写原型会失去constructor属性
prototype.constructor = subType;
subType.prototype = prototype;
}
将组合继承中的:
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
替换为:
InheritPrototype(SubType, SuperType);
寄生组合继承的优点在于,仅需调用一次SuperType构造函数。这避免了在SubType的原型上创建多余且不必要的属性。
总结
温故而知新,再次研读《JavaScript高级程序设计》中关于原型与原型链的部分,发现许多以往忽略的知识点。此次回顾并撰写文章的过程,让我受益匪浅。写文章的目的并非仅仅为了产出一篇佳作,中间的学习过程本身才是最大的享受。
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码的运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
