前端开发必须知道的JS之原型和继承
本文深入解析JavaScript中原型与继承的核心机制。详细阐述构造函数、prototype属性与实例对象__proto__的关系,通过代码示例演示类继承与原型继承的结合实现方式,并重点说明修正constructor指向的必要性,帮助开发者理解JS对象模型与继承原理。
原型与构造函数
在JavaScript中,所有函数都具备一个prototype属性,该属性引用了一个对象,即原型对象(简称原型)。这一特性不仅适用于构造函数,普通函数同样拥有原型属性。尽管我们通常关注构造函数的原型行为,但不能忽视普通函数也具备这一特征。例如,对于普通函数:
代码如下:
function F(){
alert(F.prototype instanceof Object) //true;
} 接下来探讨构造函数,即用于构造对象的函数。首先需要理解通过构造函数实例化对象的具体过程。
代码如下:
function A(x){
this.x=x;
}
var obj=new A(1); 实例化obj对象有三步:
1. 创建obj对象:obj=new Object();
2. 将obj的内部__proto__属性指向构造它的函数A的prototype属性。同时,obj.constructor等于A.prototype.constructor(这一关系始终成立,即使A.prototype不再指向原始A原型,即:类的实例对象的constructor属性永远指向“构造函数”的prototype.constructor)。这使得obj.constructor.prototype指向A.prototype(当A.prototype发生改变时,obj.constructor.prototype===A.prototype不再成立,下文会有涉及)。需注意,obj.constructor.prototype与内部的__proto__是两个不同的概念。实例化对象时使用的是__proto__,obj本身没有prototype属性,但拥有内部的__proto__,通过__proto__可以访问原型链上的原型属性和方法。Firefox浏览器公开了__proto__,可以在Firefox中通过alert(obj.__proto__)查看;
3. 将obj作为this参数调用构造函数A,从而设置成员(即对象属性和对象方法)并完成初始化。
当上述三步完成后,obj对象与构造函数A之间不再存在直接联系。此时,即使构造函数A新增任何成员,也不会影响已经实例化的obj对象。此时,obj对象拥有x属性,同时具备构造函数A的原型对象的所有成员,当然,此时该原型对象尚未包含任何成员。
原型对象初始状态为空,即不包含任何成员(包括原型属性和原型方法)。可以通过以下方法验证原型对象中包含的成员数量。
代码如下:
var num=0;
for(o in A.prototype) {
alert(o);//alert出原型属性名字
num++;
}
alert("member: " + num);//alert出原型所有成员个数。 然而,一旦定义了原型属性或原型方法,所有通过该构造函数实例化的对象都将继承这些原型属性和方法。这是通过内部的__proto__链来实现的。
例如
A.prototype.say=function(){alert("Hi")};
此时,所有A的对象都拥有了say方法。这个原型对象的say方法是唯一的副本,供所有对象共享,而不是每个对象都拥有say方法的一个独立副本。
二. 原型与继承
原型与继承
首先,观察一个简单的继承实现示例。
代码如下:
function A(x){
this.x=x;
}
function B(x,y){
this.tmpObj=A;
this.tmpObj(x);
delete this.tmpObj;
this.y=y;
} 第5、6、7行代码:创建临时属性tmpObj引用构造函数A,然后在B内部执行,执行完毕后删除。当在B内部执行this.x=x后(这里的this指向B的对象),B便拥有了x属性。需要注意的是,B的x属性与A的x属性相互独立,因此这并不构成严格的继承。第5、6、7行有更简单的实现方式,即通过call(apply)方法:A.call(this,x);
这两种方法都将this传递到A的执行上下文中,this指向的是B的对象,这就是为什么不直接调用A(x)的原因。这种继承方式被称为类继承(JavaScript中没有类的概念,这里仅指构造函数)。虽然这种方式继承了A构造对象的所有属性和方法,但无法继承A原型对象的成员。要实现这一目的,需在此基础上添加原型继承。
通过下面的例子,可以深入理解原型以及原型参与实现的完美继承。(本文核心在此^_^)
代码如下:
function A(x){
this.x = x;
}
A.prototype.a = "a";
function B(x,y){
this.y = y;
A.call(this,x);
}
B.prototype.b1 = function(){
alert("b1");
}
B.prototype = new A();
B.prototype.b2 = function(){
alert("b2");
}
B.prototype.constructor = B;
var obj = new B(1,3); 这个例子展示了B继承A的过程。第7行实现了类继承:A.call(this.x);前文已做过讲解。实现原型继承的关键在于第12行:B.prototype = new A();
这意味着将B的原型指向了A的一个实例对象。该实例对象具有x属性(值为undefined)和a属性(值为"a")。因此,B的原型也具备了这两个属性(或者说,B和A建立了原型链,B处于A的下级位置)。由于前文提到的类继承,B的实例对象也拥有了x属性。也就是说,obj对象存在两个同名的x属性,此时原型属性x需让位于实例对象属性x,因此obj.x的值为1,而非undefined。第13行定义了原型方法b2,因此B的原型也具备了b2方法。虽然第9~11行设置了原型方法b1,但你会发现第12行执行后,B的原型不再具有b1方法,即obj.b1为undefined。这是因为第12行导致B的原型指向发生改变,原先具有b1的原型对象被抛弃,自然也就失去了b1方法。
第12行执行完毕后,B的原型(B.prototype)指向了A的实例对象。而A的实例对象的构造器是构造函数A,因此B.prototype.constructor指向了构造对象A(换句话说,A构造了B的原型)。
alert(B.prototype.constructor)的结果为"function A(x){...}"。同样地,obj.constructor也指向A构造对象,alert(obj.constructor)的结果为"function A(x){...}"。这意味着B.prototype.constructor===obj.constructor(true),但B.prototype===obj.constructor.prototype(false),因为前者是B的原型,具有成员:x,a,b2;后者是A的原型,具有成员:a。如何修正这一问题呢?在第16行,将B原型的构造器重新指向B构造函数,那么B.prototype===obj.constructor.prototype(true),两者都具有成员:x,a,b2。
如果没有第16行,obj = new B(1,3)是否会去调用A构造函数进行实例化呢?答案是否定的。你会发现obj.y=3,说明仍然调用的是B构造函数进行实例化。虽然obj.constructor===A(true),但对于new B()的行为而言,执行了前述通过构造函数创建实例对象的三个步骤:第一步,创建空对象;第二步,obj.__proto__ === B.prototype,B.prototype是具有x,a,b2成员的,obj.constructor指向了B.prototype.constructor,即构造函数A;第三步,调用构造函数B去设置和初始化成员,具有了属性x,y。虽然不加第16行不影响obj的属性,但如上一段所述,却会影响obj.constructor和obj.constructor.prototype。因此,在使用了原型继承后,必须进行修正操作。
关于第12、16行,总言之,第12行使得B原型继承了A原型对象的所有成员,但也导致B的实例对象的构造器的原型指向了A原型,因此需要通过第16行修正这一缺陷。
毕了。
JavaScript异步发送HTTP请求时怎么优雅地处理错误
2026-11-05 17:10:44
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
