在编程中我们有时候需要面向不同的对象,在这些不同的对象中我们有时候需要将他们彼此关联,但是我们怎么才能做到彼此关联没呢!?现在我们就来看看JavaScript面向对象编程中的——构造函数的继承。
比如:
var Person = function(){}
Person.prototype.sex = "man";
function myfile(name,work){
this.name = name;
this.work = work;
}
这两个构造函数是有一定关系的,但是如果想让
myfile()这个函数继承上面的
Person()这个函数,那么我们该怎样做?
大体来说的话,构造函数的继承其实可分为五种方法,但是各有利弊吧,那么这五种方法分别是哪五种呢!?
一、构造函数绑定(这一种是最简单的)
说明:其实我们只需要使用call或apply这两种的任意一种方法就能够轻松的实现将父对象的构造函数绑定在了子对象上。
代码部分如下:
function Person(){
this.type = "person";
}
function myfile(name,work){
Person.apply(this,arguments); //加上这代码就能够轻松的实现
this.name = name;
this.work = work;
}
var myfile1 = new myfile("zhang","IT");
console.info(myfile1.type); //这时我们在控制台上会发现能够打印出"person"
我们看看控制台给我们的结果是什么?
我们可以看见,这个方法是能够轻松实现继承的效果的。
二、prototype原型属性模式(这种方法可能比较难以理解,但是却也是很常见的方法)
代码部分如下:(总结的时候我会截图跟代码相结合,希望能讲清楚)。
function Person(){}
Person.prototype.type = "human";
function myfile(name,work){
this.name = name;
this.work = work;
}
myfile.prototype = new Person();
myfile.prototype.constructor = myfile;
console.info(myfile.prototype);
var myfile1 = new myfile("zhang","IT");
console.info(myfile1.type);
我们来看看上面加粗后的代码在控制台上打印出的结果分别是什么如下图:
第一个打印出的是图(一)
图(一)
第二个打印出的是图二
图(二)
从跟图二图二可以看出这种prototype原型属性的方法的确是能够做到构造函数的继承的。
那么,难以理解的地方是哪里呢!我们看看下面的这两段代码:
myfile.prototype = new Person(); myfile.prototype.constructor = myfile;
其实上面这两段代码就是使用
prototype原型模式的核心所在。
首先我们来解剖一下这一段
myfile.prototype = new Person(); 代码。
为什么要将
myfile.prototype new一个实例化的对象呢!?因为我们知道new出来的实例化对像都是指向谁?是的都是指向
prototype原型属性 所指向的那个虚拟的对象,而
new Person() 指向的哪个
prototype原型属性 所指向的虚拟对象呢!?答案当然是名为Person的构造函数,现在我们应该明白了,原来
myfile.prototype = new Person(); 这一段代码是想将名为
Person的构造函数里prototype原型属性所指向的虚拟对象赋值给
myfile.prototype,这样是不是就很轻松的将两个构造函数给关联起来了呢!当然关联起来了,但是还有一个小问题,如果我们没有
myfile.prototype.constructor = myfile; 这一行代码,直接使用代码
console.info(myfile.prototype); 打印看看结果是什么样的?
如下图(三)。
有人会说,这也还是继承了啊!没出现什么问题啊!现在的构造函数
myfile()依然继承的是构造函数
myfile()的属性以及其值啊!但是你有没有发现他是继承了,但是继承的太多了,就连构造函数都已经重写了,也就是
constructor构造函数属性的属性及其属性值,都不是我们想要的,我们想要的是下面这一段代码的内容:
function myfile(name,work){
this.name = name;
this.work = work;
}
换句话说,我们其实是想继承构造函数
Person() 中用
prototype原型属性 来设置的属性
Person.prototype.type = "human"; 但是现在就连构造函数都已经继承了。为了解决这一问题我们就重写了,这个
prototype原型属性 所指向的对象中的
constructor构造函数属性,
myfile.prototype.constructor = myfile;将对象中的构造函数属性重写为我们现在的构造函数myfile(),这样就能够解决这个问题了。
现在我们在来看看图(四)重写之后的打印结果:
图(四)
是不是发现现在prototype原型属性所指向的对象中保存的就是
Person.prototype.type = "human";
function myfile(name,work){
this.name = name;
this.work = work;
}
这两个对象,一个是我们使用prototype原型属性所创建的另外一个是我们创建的名为myfile()的构造函数。
温馨提示:(其实这第二种方法理解起来的确有些麻烦,但是如果明白我的上两章讲解的内容,其实你会发现,理解起来也没那么麻烦,其实面向对象的这几章内容的关联性比较大)
上面这两章我感觉比较重要,如果你有时间并且有兴趣的话可以点进去看看,看后我相信你不仅仅能够理解这第二种方法,对理解下面的几种方法也会有很大的帮助。
三、直接继承prototype原型属性(这种方法是属于直接继承,不像上面使用new一个新的实例化对象)。
为什么要直接继承呢,因为我们在new一个新的实例化对象的时候其实是很占用空间,导致空间资源有一点点的浪费。但是这种方法有没有它自身的弊端呢!下面我们将用实例来进行讲解。
代码部分:
function Person(){}
Person.prototype.type = "human";
function myfile(name,work){
this.name = name;
this.work = work;
}
myfile.prototype = Person.prototype;
myfile.prototype.constructor = myfile;
var myfile1 = new myfile("zhang","IT");
console.info(myfile.prototype);
console.info(Person.prototype.constructor);
console.info(myfile1.type);
看看图(一)上面代码运行的结果:
图(一)
从上面的图(一)看的话感觉这个方法三还是挺靠谱的,我们用
console.info(myfile1.type); 代码执行的结果看见的确是继承了构造函数Person()中的"type"属性,并且输出的值也的确是"human",但是不知道你用代码跟图进行比对的时候有没有发现
console.info(Person.prototype.constructor); 代码执行的结果为什么输出的是myfile的构造函数?按照正常的逻辑是这个代码执行输出的结果应该是Person的构造函数啊!这里就是方法三问题的所在。
为什么会存在这个问题呢!?
原因很简单:
我们可以看见
myfile.prototype = Person.prototype; 的意思其实就是构造函数Person的原型直接赋给构造函数myfile的原型,换言之就是重写了构造函数myfile的原型。这样的话的确是继承了构造函数Person的原型了,但是原型对象中的constructor构造函数属性也随着变成了Person的constructor构造函数属性了,这不是我们想要的结果,我们知道用
myfile.prototype.constructor = myfile;这种方法就可以将原型中的构造函数重写为myfile自己的构造函数,我们在使用
myfile.prototype = Person.prototype; 方法赋值后构造函数myfile与构造函数Person都将指向同一个虚拟的对象中,所以就会造成上述的问题,我们在修该myfile的构造函数时构造函数Person也会跟着改变。
其实解决上述的这个问题也很简单,就像方法二一样我们new一个实例化对象这种问题就会隐忍而解,因为实例化的对象只是继承,而非完全的直接去继承原型,所以父对象指向的依然是它自己的构造函数,这样就不会存在继承后父级跟子集都同时指向同一个虚拟的对象,只是用了new实例化的对象来重写了子集中的对象内容,但是正如上面所说,这种方式是很占用空间内存的,于是我们就有了下面的第四种方法。
四、合理运用空对象作为其中介(其实二三四这三种方法与其说是三种方法不如说其实是一个方法,他们只是一个优化改进的过程,而这里的第四种方法就是为了解决第三种方法存在的问题)
这里我们需要一个空的对象作为中介,将父级的prototype对象赋给这个空对象,由这个空对象来完成与子对象之间的继承,这样子对象既能继承父对象的prototype对象,同时父对象的prototype对象也不会受到子对象prototype对象的影响,因为我们创建出来的又是一个空的对象所以几乎不会占用空间内存。
下面我们就来看看代码部分:
function Person(){}
Person.prototype.type = "human";
function myfile(name,work){
this.name = name;
this.work = work;
}
function F(){}
F.prototype = Person.prototype;
myfile.prototype = new F();
myfile.prototype.constructor = myfile;
console.info(myfile.prototype);
console.info(Person.prototype);
当我们打印
console.info(myfile.prototype); 代码的时候控制台给出的结果如下图:
如图所见构造函数即继承了构造函数Person的prototype原型,同时myfile自己的构造函数也依然保留,那么构造函数Person的原型是不是还依然存在有没有受到myfile这个构造函数的影响呢!?
我们来运行
console.info(Person.prototype); 这段代码看看控制台会给出什么样的结果,如下图:
我们从控制台输出的结果可以看出Person的原型依然不变。
总结:这种方法不仅解决了占用内存的问题,同时也解决了如果直接将父对象赋值给子对象后父子对象同时会指向同一个prototype原型的问题。
方法四扩展:
A:
function Person(){}
Person.prototype.type = "human";
function Myfile(name,work){
this.name = name;
this.work = work;
}
function material(Child,Parent){
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
material(Myfile,Person);
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.type);
有没有看见上面代码有什么不同?是的我们用了两个参数Child与Parent来将代码进行封装了,如果对面向对象的封装不明白的可以看看
js面向对象之封装(构造函数)
我的这一章,里面有详细说明。
B:
如果你之前看过我的prototype原型属性的这一章的话,在这章里有一个细节地方你肯定会产生疑问,在prototype原型属性的这一章里我们说过new的实例化对象时没有prototype原型属性的,prototype原型属性是在构造函数创建的时候会自动生成一个原型对象,它与对象实例化没有任何关系。但是你可以看看这行代码:
myfile.prototype = new F(); 我们运行
console.info(myfile.prototype); 这行代码在控制台上输出不仅不会报错,而且还给我们返回了我们想要的值,有没有觉得有些矛盾,这里明明是将F这个构造函数实例化了啊!?
我们运行下面的这段代码:
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.prototype);
看看下图的控制台会输出什么?
是的,输出的是undefined。
但是如果你仔细对比一下这两段代码你会发现问题的所在:
myfile.prototype = new F(); 与
var Myfile1 = new Myfile("zhang","IT");
其实new的实例化对象时真的没有prototype原型属性,但是每一个构造出来的实例化对象内部都会自动生成一个[[proto]]这个属性,正是通过这个属性来指向prototype这个原型对象中的, 而
myfile.prototype = new F(); 其实就是重写构造函数myfile的原型,而
var Myfile1 = new Myfile("zhang","IT"); 则才是真正的将实例化对象保存在一个我们创建的变量中,原型虽然被重写但是它依然还是原型故里面是存在prototype原型属性的,而真正的在变量中保存的实例化对象,那就是实力化对象所以并没有prototype原型属性,希望大家能够理解这个地方,不要混淆了。
五、拷贝继承法(何为拷贝继承法,顾名思义就是将父对象的原型拷贝也就是复制到子对象中)
我们看看下面这段代码:
function Person(){};
Person.prototype.type = "human";
Person.prototype.sex = "nan";
Person.prototype.age = "100";
Person.prototype.phone = "11111";
function Myfile(name,work){
this.name = name;
this.work = work;
}
function material(Child,Parent){
var c = Child.prototype;
var p = Parent.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}
material(Myfile,Person);
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.type);
console.info(Person.prototype.constructor);
console.info(Myfile.prototype);
其实这个方法也比较好用跟第四种方法差不多,拷贝继承的核心关键在于用
for....in 语句来循环,循环父对象中的所有属性然后复制给子对象
function material(Child,Parent){
var c = Child.prototype;
var p = Parent.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}
这个地方是关键。
下面我们就来看看用这种方法在浏览器的控制台中输出的结果是不是我们想要的。
运行
console.info(Myfile1.type); 结果如下图:
很显然是正确的,说明目前是正常的继承了Person的原型对象。
运行
console.info(Person.prototype.constructor); 我们看看Person的构造函数属性有没有被改变掉。
如下图:
哎!!!我们发现目前的Person原型中的构造函数属性依旧指向的是Person,并没有被改变,挺好。
运行
console.info(Myfile.prototype); 我们看看现在构造函数Myfile的原型中都保存着哪些东西。
如下图:
我们可以很明显的发现现在的构造函数Myfile的原型中保存的是从Person这里继承来的所有对象,以及自己的构造函数属性中的全部对象。
总结:其实
for....in 循环的是父对象中的所有属性,然后将循环的属性都保留在了变量
i 中,这样就实现了循环复制的效果,而不像方法二方法三中那样,是将父对象中整个的原型都赋值给了子对子对象,而方法五是只复制了属性,但是constructor构造函数属性却没有被复制,换言之Person函数与Myfile函数依然在constructor构造函数属性中依然还是独立的个体他们并没有方法三一样同时指向了同一个原型对象。
我们来看看
i 中都保存了什么,你就会恍然大悟,我们运行如下代码:
function material(Child,Parent){
var c = Child.prototype;
var p = Parent.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}
看看控制台会给我们什么样的结果:
这里就是
变量i 中保存的属性,我们看见了这里并没有原型中constructor构造函数属性。
友情提示:在我的面向对象讲解的章节中,会出现一些我没有过多讲解的小属性,比如
uber属性 或是在控制台中会出现
__proto__属性,这些东西都是些什么?有什么作用?我会在将面向对象剩下的内容讲完后,专门用一章的篇幅对这些小属性进行讲解,其实这个不会对理解造成影响,就当是对面向对象的补充吧。