本文深入解析JavaScript原型链机制,阐明实例对象、构造函数与原型对象之间的[[wp:a7]]联系。详细对比了利用call借用构造函数、原型链指向、组合继承、拷贝继承、直接继承prototype以及空对象中介等6种继承模式的优缺点与实现细节,帮助开发者掌握JS面向对象编程核心。
本文详细介绍JavaScript中原型链的工作原理,并深入探讨如何利用原型特性实现对象继承。内容包含原理分析与多种实现代码示例,供开发者参考学习。
原型链基础与语法
原型链基础
原型链本质上是实例对象与原型对象之间建立的一种关联关系,这种关联通过原型属性进行连接。
在JavaScript中,实例对象内部包含属性,该属性指向原型对象。需要注意的是,并非标准属性,而是浏览器内部使用的机制,部分旧版浏览器可能不支持。同时,构造函数中定义的prototype属性也指向原型对象。
需要特别注意的是,定义在原型对象中的方法可以相互调用访问。
代码示例
function Animal(name,age){
this.name=name;
thia.age=age;
}
//在原型中添加方法
Animal.prototype.eat=function(){
console.log("动物吃草")
this.play()
}
Animal.prototype.play=function(){
console.log("玩啥呢")
}
原型的基本语法结构
通过原型对象实现数据共享
第一种实现方式
function Student(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
}
Student.prototype.height="188"
Student.prototype.weight="55kg"
Student.prototype.study=function(){
console.log("好好学习i")
}
var stu=new Student("小红",20,"男")
console.dir(stu)
运行结果

第二种实现方式
function Student(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
}
Student.prototype={
height:"188",
weight:"55kg",
study:function(){
console.log("好好学习i")
}
}
var stu=new Student("小红",20,"男")
console.dir(stu)
运行结果

对比上述两种写法,可以发现存在显著差异。第二种写法会覆盖原型对象,导致constructor构造器属性丢失,因此必须手动修正构造器的指向。
修正后的完整代码
function Student(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
}
Student.prototype={
constructor:Student,
height:"188",
weight:"55kg",
study:function(){
console.log("好好学习i")
}
}
var stu=new Student("小红",20,"男")
console.dir(stu)
经过修正,构造器指向恢复正常。

实例对象属性与方法查找规则
实例对象属性与方法查找规则
当访问实例对象的属性或方法时,JavaScript引擎遵循以下查找顺序:首先在实例自身中查找,若存在则直接使用;若不存在,则通过
__proto__指向的原型对象进行查找;如果原型中仍未找到,则继续沿着__proto__向上追溯,直至原型链末端未找到时抛出错误。
对象间关系与原型指向可变性
构造函数、实例对象与原型对象的关系
构造函数用于实例化对象。构造函数内部包含一个prototype属性,指向该构造函数的原型对象。该原型对象中包含一个constructor属性,指向构造函数本身。此外,实例对象的属性指向构造函数的prototype属性。因此,定义在构造函数原型对象中的方法,可以被其实例对象直接访问。
原型指向是否可变?
在探讨原型指向变化之前,需明确构造函数和实例对象中this关键字的指向含义。
此处定义一个自定义构造函数Person,并在其内部输出this的值。
同时在Person的原型对象上添加一个eat方法,并在该方法内部输出this。
接着实例化一个对象,并调用该eat方法。
执行代码以查看具体结果。

控制台输出结果

基于执行结果分析
原型对象中方法内部的this指向当前实例对象。
构造函数内部的this同样指向当前实例对象。
接下来尝试修改原型的指向。

代码中首先定义了一个Person自定义构造函数,并在其原型上添加了eat方法。
随后定义了一个Student函数,并在其原型上定义了sayHi方法。
将Student的原型指向Person的一个实例对象。
接着实例化一个Student对象,并在该实例上尝试调用eat方法和sayHi方法。
运行结果

由此可以确认,通过上述代码成功改变了stu实例对象的原型指向。
Student.prototype=new Person(10);
总结
原型的指向是可以被动态改变的。
实例对象的属性始终指向其所属构造函数的原型对象。
如果构造函数的prototype属性指向发生更改,实例对象的指向也会随之改变。
实例对象与原型对象之间的联系是通过属性建立的,这种层级关系即被称为原型链。
在修改原型指向后,应当立即添加新的原型方法。
此时,sayHi方法将被创建在new Person(10)这个实例对象上。
原型链最终指向分析
原型链的最终指向位置
实例对象中的__proto__属性指向构造函数的prototype属性。
以以下代码为例进行分析。

进行测试验证


结论如下
per实例对象的指向Person.prototype,Person.prototype的指向Object.prototype,而Object.prototype的最终指向Null。
查看HTML DOM对象的原型链结构,可以发现其具有有趣的层级关系。

此处展示相关图示。

JavaScript继承模式详解
实现对象继承
补充知识:instanceof运算符的判断逻辑
判断逻辑为:从左侧操作数的__proto__路径出发,与右侧操作数的prototype属性进行比较,如果两条路径最终指向同一个引用对象,则返回true。
1. 利用call借用构造函数实现继承
优点:成功实现了属性的继承,且各实例的属性值互不相同。
缺点:无法继承父级原型上定义的方法。
function Person(name,age,sex,weight){
this.name=name;
this.age=age;
this.sex=sex;
this.weight=weight;
}
Person.prototype.sayHi=function(){
console.log("您好")
}
function Student(name,age,sex,weight,score){
//将当前实例对象传入Person 借过来使用一次来达到继承效果
Person.call(this,name,age,sex,weight);
this.score=score;
}
var stu1=new Student("小明",10,"男","10kg","100")
2. 原型链方式实现继承
通过将Student的prototype指向Person实例来实现继承效果。
优点:成功继承了父级原型上的方法。
缺点:所有Student实例必须共享相同的name和age属性,无法拥有独立的状态。
Student.prototype.constructor=Student
注意:使用原型继承时,必须将构造器的指向修正回正确的构造函数。
function Person(name,age){
this.name=name;
this.age=age;
}
Person.prototype.eat=function(){
console.log("Person 吃饭")
}
function Student(num,score){
this.num=num
this.score=score
}
//继承
Student.prototype=new Person("小红",10)
Student.prototype.constructor=Student
var stu =new Student(2016002288,80)
stu.eat()//Person 吃饭
3. 组合继承模式
组合继承结合了上述两种方法的优点,既实现了属性继承,又保留了原型方法的复用。
function Person(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
}
Person.prototype.sayHi=function(){
console.log("你好")
}
function Student(name,age,sex,score){
//借用构造函数
Person.call(this,name,age,sex)
this.score=score
}
// 改变了原型指向
Student.prototype=new Person();//不传值
Student.prototype.eat=function(){
console.log("吃东西");
}
var stu=new Student("小黑",20,"男","100分")
console.log(stu.name,stu.age,stu.sex,stu.score);
stu.sayHi()//你好
stu.eat()//吃东西
4. 拷贝继承模式
该方式类似于复制操作,将一个对象中的属性和方法直接复制到另一个对象中。
function Person(){
}
Person.prototype.name="小红"
Person.prototype.age=18
function Student(){
}
var p=Person.prototype;
var s=Student.prototype;
for(key in p){
s[key]=p[key]
}
console.dir(Student)
控制台输出示例

每次使用for in循环较为繁琐,可以对其进行优化封装。
function extend(Child,Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
//这个属性直接指向父对象的prototype属性,可以直接调用父对象的方法,为了实现继承的完备性,纯属备用性质
c.par = p;
}
5. 直接继承prototype
优点:执行效率较高。
缺点:由于涉及传址引用,修改Student的属性可能会意外影响Person的属性。
function Person(){};
Person.prototype.name="小红";
Person.prototype.age=18;
function Student(){};
Student.prototype=Person.prototype;
console.dir(Student);
console.dir(Person);
Student.prototype.age=25;
控制台输出示例

6. 利用空对象作为中介实现继承
通过这种方式修改Student的prototype,可以避免对Person的prototype产生副作用。
function Person(){};
Person.prototype.name="小红";
Person.prototype.age=11;
function Student(){};
var F=function(){};
F.prototype=Person.prototype;
Student.prototype=new F();
Student.prototype.constructor=Student;
Student.prototype.age=25;
console.dir(Person)
console.dir(Student)
控制台输出示例

对继承逻辑进行封装
function extend(Child,Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.par = Parent.prototype;
}
更多关于JavaScript的内容,可参考本站相关教程:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》。
希望本文所述内容能对您的JavaScript程序设计有所帮助。