JavaScript中的原型(链)&继承(二)

有点难受啊,我发现写博客,内容自己必须要懂,必须保证正确性。而且让读者在看的过程中,知道我说了什么,表达了什么,是一件相当不容易的事啊。然而这是最重要的一件事。。

原型链

原型链基本概念

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如,我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,层层递进,构成了实例与原型的链条。(实例赋给原型是原型链接的关键)

原型链的维护

通过上篇的理论,我们知道构造器中有一个名为prototype的成员,维护原型链不只是构造器的事,一个实例至少应该拥有指向原型的proto属性,这是JavaScript中的对象系统基础。尽管如此,但我们不能直接访问这个不可见的属性,该属性的存在构成了一个“内部原型链”,而构造器的prototype所组成的“构造器原型链”(亦即我们通常所说的“原型链”)与它是有区别的。

首先我们想通过构造一个原型继承的对象系统,进行说明:

//构造器申明
function MyObject(){
}
function MyObjectEx(){
}
//原型链
MyObjectEx.prototype = new MyObject();//实例指向了原型对象
//创建对象实例
obj1 = new MyObjectEx();
obj2 = new MyObjectEx();

上述代码,实际上通过构造器显示的prototype属性构建了一个原型链,而对象实例也通过内部属性proto构建了一个原型链。由于obj1.proto是一个不可访问的内部属性,所以没有办法从obj(指代所有MyObjectEx的实例)开始访问整个原型链。

constructor属性的维护

概念理解:原型对象和对象实例的概念在一定情况下可以互相转换(依据来自,当实现原型继承的时候,要将一个对象实例直接赋值给特定构造函数的原型对象,而该对象实例重写了这个原型对象,新的原型对象就是该对象实例。新的原型对象有一个指向原来原型对象的指针。之前说到对象实例中的内部属性是指向原型对象的指针。而且这个新的原型对象将会通过构造函数创建新的实例,新的实例重写想要继承该实例属性的原型对象。现在看来二者在继承过程中实现了转换,对象实例和原型对象都有constructor属性,,而且二者之前说过存在“映射”关系,更可以说明这一点)。但二者是不等价的。(繁杂不堪,大家可以不看,个人见解而已)

一个构造器产生的实例,其constructor属性默认总是指向该构造器。原型对象的constructor属性也默认总是指向指向它的构造器

可以这样理解,因为JavaScript的实例创建的过程中实际上是“复制”原型对象的过程,原型对象中的constructor属性指向的一定是相应的构造器,同理实例也得“映射”原型对象属性constructor的行为。(个人观点)

补充:实例中的constructor属性与[[Prototype]]属性完全不同,我在理解的时候老是会把二者的作用混为一谈。原型对象的constructor属性指向相应的构造函数,构造函数的实例的_proto_(内部属性)指向原型对象。而原型对象与对象实例可以互换,所以,对象实例的constructor属性指向创建它的构造函数,对象实例的内部属性指向原型对象。

示例如下:

//构造器的constructor属性
function MyObject(){
}
var obj = new MyObject();
alert(obj.constructor === MyObject);//显示为true

究其根源,在于构造器(函数)的原型的constructor属性指向了构造器本身,所以下面的代码也显示为true

alert(MyObject.prototype.constructor === MyObject);

由此可见,我们不必担心原型链的维护,JavaScript事实上已经为构造器维护了原型属性。因此在一般情况下,我们可以通过实例constructor属性来找到构造器,并进而找到它的原型。如下例:

alert(obj.constructor.prototype);

于是我们可以在“内部原型链和构造器原型链”中找到一个连接点,使得我们在实例不能访问obj.proto的情况下,通过构造器来访问原型链。

constructor存在的问题

下面两个构造器的prototype.constructor毫无疑问都指向构造器自身。

//构造器的constructor属性
function MyObject(){
}//MyObject.prototype.constructor = MyObject;
function MyObjectEx(){
}//MyObjectEX.prototype.constructor = MyObjectEx;

//等效于下面的代码
proto = new MyObject();//proto.constructor = MyObject;
MyObjectEx.prototype = proto;//MyObjectEx.prototype.constructor = proto.constructor = MyObject;

//构建原型链
MyObjectEx.prototype = new MyObject();

上述代码出现了问题,构造了原型链之后,MyObjectEx的原型属性被重写。

var obj1 = new MyObject();
var obj2 = new MyObjectEx();
alert(obj1.constructor == obj2.constructor);//显示true

结果是:obj1与obj2是不同的构造器产生的实例,而它们的constructor属性却指向了相同的构造器。
这要求我们在给MyObjectEx赋予一个原型(对象实例)时,应该修正该原型的构造器值。为此一般的建议(例如《JavaScript权威指南》)进行如下处理:

//方法一,直接构建原型链
MyObjectEx.prototype = new MyObject();//原型被重写,本来指向构造函数MyObjectEx,重写后指向实例所指向的构造函数,即MyObject;
alert(MyObjectEx.prototype.constructor === MyObject);//true
MyObjectEx.prototype.constructor = MyObjectEx;

也就是在重写原型后,再修改原型的constructor属性,使之指向正确的构造函数器。修改后,我们切断了与原型父类的关系。

出于原型继承的目的,obj1、obj2等实例需要属性正确的指向MyObjectEx()构造器。而如果要正确地访问原型链,则应该指向MyObject()构造器,这样的话与之前切断原型父类关系自相矛盾。

另一个方法是保持原型的构造器属性,而在子类构造函数内初始化实例的构造器属性。。

//方法2:正确维护constructor,以便回溯原型链
function MyObjectEx(){
    this.constructor = arguments.callee;
    //or,this.constructor = MyObjectEx;
}
//原型链
MyObjectEx.prototype = new MyObject();

这样一来,MyObjectEx()构造的实例的constructor属性都能正确地指向MyObjectEx(),而原型的constructor则指向MyObject()。该方法每次构造实例时都要重写constructor属性。如果不需要回溯原型链,可以使用第一种方法。

内部原型链的作用

用户代码只需要正确维护constructor属性就可以回溯原型链,实例的内部属性显得没有价值。
这个问题与原型继承的实质有关,也与面向对象的实质有关。面向对象的继承性约定:子类与父类具有相似性。在原型继承中,这种相似性是在构造时决定的,也就是由new()运算内部那个“复制”操作决定的。因此,子类实例有一个特性:不能用delete删除从父类继承来的成员。也就是说子类必须具有父类的特性。即使你可以重写成员,改变它的实现,但在界面上必然与父类一致。

为了达成这种一致性,且保证它不被修改。JavaScript使对象实例在内部持有这样一个proto属性,并且不允许用户访问。这样用户可以出于任何目的来修改constructor属性,而不用担心实例与父类的一致性。

简单地说,内部原型链是JavaScript的原型继承机制所需的。而通过constructor与prototype所维护的构造器原型链,则是用户回溯代码时才需要的。如果用户无需回溯,可以不维护,使用上述代码段中的方法1即可。

猜你喜欢

转载自blog.csdn.net/hcjs_zee/article/details/71302276