剖析js原型与原型链

原型模式

  我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:

原型对象和实例的关系有点像JAVA中父类子类的关系,有些继承的味道。

function persion(){
}

persion.prototype.name = "Nicholas";
persion.prototype.age = 29;
persion.prototype.job = "Software Engineer";
persion.prototype.sayName = function () {
    alert(this.name);
};

var person1 = new persion();
person1.sayName(); //"Nicholas"

var person2 = new persion();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //"true"

  在此,我们将sayName()方法和所有属性直接添加到了person的prototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。要理解原型模式的工作原理,必须先理解ECMAScript中原型对象的性质。

1.理解原型对象

  无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
  创建了自定义的构造函数之后,其原型对象默认只会取得 constructor属性;至于其他方法,则都是从object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性proto;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
  以前面使用Person构造函数和 Person.prototype创建实例的代码为例,图6-1展示了各个对象之间的关系。
在这里插入图片描述  图6-1展示了person构造函数、person的原型属性以及person现有的两个实例之间的关系。在此,person.prototype 指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。person的每个实例——person1和person2都包含一个内部属性,该属性仅仅指向了person.prototype;换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。

这可以这么理解,构造函数是一个空函数。创建一个实例的代码虽然是构造函数new出来的,可实际上是通过原型对象创建出来的,实例与构造函数并没有直接的关系,实例是基于原型对象创建的,而原型对象的constructor指向构造函数。

  虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeof()方法的对象(person.prototype),那么这个方法就返回true,如下所示:

alert(Person.prototype.isPrototypeof(person1));//true
alert(Person.prototype.isPrototypeof(person2));//true

  这里,我们用原型对象的isPrototypeof()方法测试了person1和person2。因为它们内部都有一个指向Person.prototype的指针,因此都返回了true。
  ECMAScript5增加了一个新方法,叫object.getPrototypeof(),在所有支持的实现中,这个方法返回t[Prototype]]的值。例如:

alert(Object.getPrototypeof(personl)==Person.prototype);//true 
alert(Object.getPrototypeof(person1). name);//"Nicholas"

  这里的第一行代码只是确定object.getPrototypeof()返回的对象实际就是这个对象的原型。第二行代码取得了原型对象中name属性的值,也就是“Nicholas"。使用object.getPrototypeof()可以方便地取得一个对象的原型,而这在利用原型实现继承(本章稍后会讨论)的情况下是非常重要的。支持这个方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera12+和Chrome。
  每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例 person1有sayName属性吗?”答:“没有。”然后,它继续搜索,再问:“person1的原型有sayName属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。当我们调用person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。

  虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。来看下面的例子。

function Person(){
Person. prototype. name="Nicholas"; 
Person. prototype. age=29; 
Person. prototype. job="Software Engineer"; 
Person. prototype, sayName=function()(
alert(this. name);
}; 

var personl=new Person();
var person2=new Person();
peraonl.name =nGreg";alert(person1.name);//"Greg"—来自实例
alert(person2.name);//"Nicholaa"——来自原型

  在这个例子中,person1的name 被一个新值给屏蔽了。但无论访问person1.name还是访问person2.name都能够正常地返回值,即分别是“Greg*(来自对象实例)和“wicholas”(来自原型)。当在alert()中访问 person1.name时,需要读取它的值,因此就会在这个实例上搜索一个名为name的属性。这个属性确实存在,于是就返回它的值而不必再搜索原型了。当以同样的方式访问 person2.name时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了name-属性。
 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为nu11,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。

可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果实例中添加的属性与实例原型中一样则会断开该属性指向原型的连接

不过,使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性,如下所示。

function Person(){
}
Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="Software Engineer";
Person.prototype.sayName=function(){
alert(this.name);
};

var personl=new Person();
var pexson2=new Person();

person1.name="Greg";
alert(personl.name);//"Greg“——来自实例
alert(person2.name);//"Nicholas“——来自原型

delete personl.name;
alert(peraonl.name);//"Nicholaa——来自原型

  在这个修改后的例子中,我们使用delete操作符删除了person1.name,之前它保存的“Greg"值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中name属性的连接。因此,接下来再调用person1.name时,返回的就是原型中name属性的值了。

原型链

  ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

可以简单理解成一个环套一个环和铁链一样。第一个环里的原型对象是第二个环里的实例,第二个环里的原型对象又是第三环里的实例则一环套一环。

  实现原型链有一种基本模式,其代码大致如下。

function SuperType(){
this. property=true;
}

Superrype.prototype.get SuperValue=function(){
return this.property;
};

function SubType(){
this.subproperty=false;
}

//继承了SuperType 
Subrype.prototype=newSuperlype();

SubType.prototype.getSubValue=function(){
return this.subproperty;;

var instance=new SubType();
alert(instance.getSuperValue());//true

  以上代码定义了两个类型:Superrype和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是Subrype继承了superrype,而继承是通过创建Superrype的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于Superrype的实例中的所有属性和方法,现在也存在于Sublype.prototype中了。在确立了继承关系之后,我们给SubType.prototype 添加了一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。这个例子中的实例以及构造函数和原型之间的关系如图6-4所示。
在这里插入图片描述  在上面的代码中,我们没有使用Subype默认提供的原型,而是给它换了一个新原型;这个新原型就是superrype的实例。于是,新原型不仅具有作为一个Superrype的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了superrype的原型。最终结果就是这样的:instance指向 Subrype的原型,subrype的原型又指向Superrype的原型。getSupervalue()方法仍然还在SuperType.prototype中,但property 则位于SubType.prototype中。这是因为property是一个实例属性,而 getSuperValue()则是一个原型方法。既然 SubType.prototype 现在是Superrype的实例,那么property当然就位于该实例中了。此外,要注意instance.constructor现在指向的是Supertype,这是因为原来subrype.prototype中的 constructor被重写了的缘故。
  通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。读者大概还记得,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索Subrype.prototype;3)搜索SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

参考书籍:JavaScript高级程序设计(第三版)

猜你喜欢

转载自blog.csdn.net/qq_42068550/article/details/89423101