prototype
可谓是javascript
中的难点和重点了,很多人对prototype
是敬而远之,本文将以一种诙谐幽默的语言来介绍prototype
在本文中,我们讲解prototype
的内容主要由:什么是prototype
,prototype
与函数之间的关系,prototype
与实例对象之间的关系,使用proto
实现一个简单的继承。
- prototype的简要介绍
在javascript
中,创建的每个函数天生都自带着一个prototype
属性。这里我们要强调的是:这个prototype
属性是一个指针,指向一个对象,在这里,我们称指向的这个看不到但确实存在的对象为原型对象。其实可以用下面一个简单的例子来说明:
var proto = {
name : 'zhaoran',
age : 25
}
function Person(){
}
Person.prototype = proto;
这样Person.protptype
就指向到了proto
,如果还有其他的引用(A)也指向到了proto,那么这个引用和Person.prototype
就是相等的:A==Person.prototype
;
prototype
指向的这个对象是真实存在的,可能很多的同学对prototype
属性和原型对象有些混淆,我们在这里把原型对象叫做大SB
。大SB
与普通对象的一个不同之处就是,他也有一个天生的属性:constructor
,这个constructor
重新指回到了函数。不过,既然大SB
也是一个对象,那么它也是继承于Object
的,拥有Object
所有的方法和属性,比如toString()
等。
大SB = {
constructor : Person,
say : function(){
return "hello world";
}
}
那么现在我们就能得到两个引用关系:
大SB = Person.prototype; // 原型对象 = 函数.prototype;
Person = 大SB.constructor(Person.prototype.constructor); // 函数 = 原型对象.constructor
2. 对象实例和prototype中的属性
在构造函数能设置属性和方法,在prototype中也能设置属性和方法,那new出的对象使用的是哪个呢?我们来看一个例子:
function Person(){
this.name = "zhaoran";
}
Person.prototype.name = "bing";
Person.prototype.say = function(){
return "hello world";
}
var John = new Person();
alert(John.name); // "zhaoran"
alert(John.say()); // "hello world"
从运行的结果我们可以看到,John.name
输出的是构造函数中的属性值"zhaoran",John.say()
输出的是"hello world"
。这是因为,当读取某个对象的属性值时,会首先在实例对象中进行搜索,若搜索到则直接进行返回;若搜索不到则去原型对象中进行寻找。因此在使用John
调用say()
方法时是正确的,不会报错。而且是进行了两次的搜索。
虽然say()
是挂载在prototype
上,John
也同样能直接进行访问。但是,我们不能直接对John.say
进行修改从而影响prototype
的say()
方法,如下:
John.say = function(){
return "this has changed";
}
John.say(); // "this has changed"
var Tom = new Person();
Tom.say(); // "hello world"
从运行John.say()
的结果来看,say()
方法确实发生了变化。但是,当我们再new
出一个新对象Tom
时,Tom.say()
返回的还是"hello world"
,这是为什么呢?
因为,对John.say()
进行修改时,不是修改了prototype
上的say()
,而是给John
这个实例对象添加了一个say()
方法,从而屏蔽了prototype
上的say()
方法,由上面的寻找顺序我们可以知道,若实例对象存在这个方法就直接返回了,不再去原型对象上寻找。而new
出的新对象Tom
在调用say()
方法时,Tom
自己是没有say()
方法的,只能去原型对象上寻找,因此返回的还是"hello world"
。所以,对John.say
进行修改时,不是修改了prototype
上的say()
方法,而是给John
添加了实例方法,屏蔽掉了prototype
上的方法而已。那如何才能修改prototype
上的方法呢,好办,直接在prototype
上修改:
Person.prototype.say = function(){
return "zhaoran's blog";
}
这样就能修改prototype
的say()
方法了。
让我们从图里看看这些的关系,懒得画图了,从网上盗了一张:
![](https://img.haomeiwen.com/i9596322/5c85f61f3dcb4539.png)
最左边是两个实例对象,都有
__proto__
属性,指向中间的原型对象;中间的原型对象,有constructor
属性,指回到函数Foo
,同时因为他也是对象,也有__proto__
属性,指向超类Object.prototype
;上面的Foo()
函数的prototype
指向中间的原型对象,Foo()
的__proto__
指向到Function.prototype
;下面的是超类Object
的原型对象。
3. 重写prototype
在上面的例子中,我们都是大部分给prototype
添加属性或方法,本来prototype
指向的是大SB
,若我们给prototype
添加属性或方法时,就是给大SB
添加属性或方法:
Person.prototype.name = "zhaoran";
而大SB
里其他的属性和方法是不受影响的,constructor
依然指回到Person
。但是,若我们这样写:
Person.prototype = {
name : "zhaoran",
say : function(){
return "my name is name"
}
}
就是对Person
的prototype
重写了,让prototype
进行了重新的指向,本来Person.prototype
指向的是大SB
,可是现在却指向了CTM
,而CTM
里也没有constructor
属性指回到Person
,若是想重新指回到Person
,还得手动添加一个constructor
属性:
Person.prototype = {
constructor : Person, // 重新指回到Person
name : "zhaoran",
say : function(){
return "my name is name"
}
}
这样就能手动构造出新的原型对象了,new
出的实例对象也能使用这个prototype
上的属性和方法。
但是这里还存在一个问题,若之前已经有new
出的实例对象,然后再修改Person.prototype
,之前的实例对象是无法使用新的原型对象(CTM)
上的属性和方法的,因为之前的实例对象的__proto__
指向的依然是大SB
。因此,请慎重重写prototype
4. 原型继承
说到prototype
就不得说prototype
继承,我们通过给prototype
上添加属性和方法,就能使该构造函数所有的实例对象拥有属性和方法。我们先来看下面的代码:
function Father(){
this.name = "father";
this.age = 43;
}
Father.prototype.job = "Doctor";
Father.prototype.getName = function(){
return this.name;
}
function Son(){
this.name = "son";
}
Son.prototype = new Father(); // Son的prototype指向Father的实例对象
var John = new Son();
for(var k in John){
console.log(k+' : '+john[k]);
}
/*
输出结果:
name : son
age : 43
job : Doctor
getName : function (){
return this.name;
}
*/
从上面的例子中可以看到,Son
的实例对象继承了Father
中所有的属性和方法。当然,若Son
的对象实例中存在的,还依然保留。不过Son
原型中的属性和方法是会被彻底覆盖的。我们来分析一下这是为什么?
Son
的prototype
指向的是Father
的一个实例,我们把这拆成两步:
var father = new Father();
Son.prototype = father;
在实例father
中既有name,age
属性,也有Father.prototype
中的属性和方法,我们对father
循环看一下:
for(var k in father){
console.log(k, father[k]);
}
/*
name father
age 43
job Doctor
getName Father.getName()
*/
由于constructor
是不可枚举的类型,在for~in
循环里是输出不了constructor
的,其实father
是这样的:
father = {
constructor : Father,
name : father
age : 43
job : Doctor
getName : Father.getName()
}
因此Son
的prototype
指向的就是上面father
的内容。到此,Son
的prototype
中的constructor
的指向也发生了改变,本来Son.prototype
是指向到Son
的,现在指向到了Father
。即完全重写了Son
的prototype
,重新指向了另一个引用。所以,我们就能知道为什么对象实例中的属性能够保存,而prototype
中的会彻底被删除了。
如果想给Son
添加原型方法或属性,那只能是在Son.prototype = new Father()
;之后进行添加:
Son.prototype = new Father();
Son.prototype.getJob = function(){
return this.job;
}
这样就能添加上getJob
方法了。
到这里,会有人问,如果Son.prototype = Father.prototype
会怎么样呢?我们刚才也说了Father.prototype
指向的是大SB
,而大SB
也是一个实例对象,是Object
的一个实例,这就相当于:
Son.prototype == 大SB; // 指向了大SB
因此,如果是Son.prototype = Father.prototype
的话,那么Son
只能继承到Father.prototype
上的job
和getName()
,其他的属性是获取不到的。
网友评论