在上一篇文章中,我们介绍了原型的概念,并学习了javascript中三个好朋友之间的关系:构造函数、原型对象和实例:每个构造函数都有一个“守护神”——原型对象,原型对象在它的心中也有一个构造函数的“位置”,两个一类,而实例偷偷爱着原型对象,她也在心中保持着一个原型对象的位置
Javascript本身不是一种面向对象的语言,而是一种基于对象的语言。对于习惯其他OO语言的人来说,一开始并不适合,因为这里没有“类”的概念,或者说“类”和“实例”是无法区分的,更不用说“父类”和“子类”了。那么,javascript中的这一堆对象是如何关联的呢?幸运的是,javascript在设计之初就提供了“继承”的实现。在了解“继承”之前,首先要了解原型链的概念。
原型链
我们知道原型都有一个指向构造函数的指针。如果我们让SubClass原型对象等于另一个类型实例new SuperClass()?此时,SubClass原型对象包含一个指向SuperClass原型的指针,SuperClass原型也包含一个指向SuperClass构造函数的指针。这样就形成了原型链。
具体代码如下:
function SuperClass(){ this . name=' women ' } SuperClass . prototype . sayWhat=function(){ return this . name ' :我是女生!';} function SubClass(){ this . subname=' your sis ter ';} SubClass . prototype=new SubClass();subclass . prototype . subsaywhat=function(){ return this . subname ' : I '是个漂亮的女孩';} var sub=new SubCe();console . log(sub . SayWhat());//女人我是个女孩!用原型链实现继承
从上面的代码可以看出,SubClass继承了SuperClass的属性和方法,这种继承是通过给SubClass的原型对象分配SuperClass实例来实现的,这样SubClass的原型对象就被SuperClass的一个实例所覆盖,拥有它的所有属性和方法,并且还拥有一个指向SuperClass原型对象的指针。
在使用原型链实现继承时,有一些事情需要注意:
注意继承后构造函数的变化。这里,sub的构造函数指向SuperClass,因为SubClass的原型指向SuperClass的原型。在理解原型链的时候,不要忽略末端有一个默认的Object对象,这就是为什么我们可以在所有对象中使用toString等内置方法。
当通过原型链继承时,不能使用文字量来定义原型方法,因为这将重写原型对象(在上一篇文章中也介绍过):
function SuperClass(){ this . name=' women ' } SuperClass . prototype . sayWhat=function(){ return this . name ' :我是女生!';} function SubClass(){ this . subname=' your sis ter ';} SubClass . prototype=new SubClass();SubClass.prototype={//此处的prototype对象被覆盖,因为它不能继承SuperClass属性和方法subsaywhat : function(){返回此。subname ' :i`m我是一个美丽的女孩;} } var sub=new SubCe();console . log(sub . SayWhat());//typeerror : undefined不是函数实例共享问题。在解释原型和构造函数时,我们已经介绍了包含引用类型属性的原型将由所有实例共享。同样,我们继承的原型也将共享“父类”原型的引用类型属性。当我们通过原型继承修改“父类”的引用类型属性时,从原型继承的所有其他实例都会受到影响,这不仅浪费资源,也是我们不想看到的现象:
function SuPerClass(){ this . name=' women ';this.bra=['a ',' b '];} function SubClass(){ this . subname=' your sis ter ';} SubClass . prototype=new SubClass();var sub 1=new SubClass();sub 1 . name=' man ';sub 1 . bra . push(' c ');console . log(sub 1 . name);//man console . log(sub 1 . bra);//['a ',' b ',' c ']var sub 2=new SubClass();console . log(sub 1 . name);//女人控制台. log(sub 2 . bra);//['a ',' b ',' c']注意:当一个元素添加到数组中时,从SuperClass继承的所有实例都会受到影响,但是如果修改name属性,其他实例不会受到影响,因为数组是引用类型,而名称是基本类型。如何解决实例共享的问题?让我们继续往下看.
经典继承(构造函数窃取)
正如我们介绍的,我们很少单独使用原型来定义对象,我们在实际开发中也很少单独使用原型链。为了解决引用类型共享的问题,javascript开发人员引入了经典的继承模式(也称为借用构造函数继承),其实现非常简单,即在子类型构造函数中调用超类型构造函数。我们需要使用javascript提供的call()或apply()函数。让我们看看下面的例子:
function SuPerClass(){ this . name=' women ';this.bra=['a ',' b '];} function SubClass(){ this . subname=' your sis ter ';//将SuperClass的作用域赋予当前构造函数继承super class . call(this);} var sub 1=new SubClass();sub 1 . bra . push(' c ');console . log(sub 1 . bra);//['a ',' b ',' c ']var sub 2=new SubClass();console . log(sub 2 . bra);//['a ',' b ']super class . call(this);这句话的意思是在SubClass的实例(上下文)环境中调用SuperClass构造函数的初始化工作,这样每个实例都会有自己的bra属性的副本,不会互相影响。然而,这种实现仍然不够完善。既然引入了构造函数,我们也面临着上一篇文章中提到的构造函数的问题:如果构造函数中有一个方法定义,那么每个实例都有一个单独的Function引用。我们的目的是共享这个方法,我们在超类型原型中定义的方法不能在子类型实例中调用:
function SuPerClass(){ this . name=' women ';this.bra=['a ',' b '];} superclass . prototype . sayWhat=function(){ console . log(' hello ');} function SubClass(){ this . subname=' your sis ter ';super class . call(this);} var sub 1=new SubClass();console . log(sub 1 . SayWhat());//TypeError : Undefined不是函数如果你看了前面关于原型对象和构造函数的文章,一定已经知道这个问题的答案了,那就是遵循前面的套路,使用“组合拳”!
组合遗传
组合继承是结合原型链和构造函数的优点实现继承的一种方式。简单来说就是用原型链继承属性和方法,用借用的构造函数实现实例属性的继承,既解决了实例属性共享的问题,又允许继承超类型属性和方法:
function SuPerClass(){ this . name=' women ';this.bra=['a ',' b '];} superclass . prototype . sayWhat=function(){ console . log(' hello ');} function SubClass(){ this . subname=' your sis ter ';super class . call(this);//调用超类}子类。prototype=newsuperclass()第二次;//第一次调用super class var sub 1=new SubClass();console . log(sub 1 . SayWhat());//hello组合继承也是实际开发中最常用的实现继承的方式,可以满足你的实际开发需求,但是人们对完美的追求是没有止境的,所以有人会对这个模式吹毛求疵:你在这个模式中已经两次调用了超类型构造函数!两次。你成功了吗?放大100倍是多少性能损失?最有力的反驳就是想出解决办法。幸运的是,开发人员找到了解决这个问题的最佳方案:
寄生组合遗传
在介绍这个继承方法之前,我们先了解寄生构造函数的概念。寄生构造器类似于上面提到的工厂模式。它的思想是定义一个公共函数,专门用来处理对象的创建,并在创建后返回这个对象。这个函数非常类似于构造函数,但是构造函数没有返回值:
函数Gf(名称,bra){ var obj=new Object();obj.name=nameobj.bra=braobj . SayWho=function(){ console . log(this . name);}返回obj}var gf1=新Gf('bingbing ',' c ');console . log(gf1 . SayWhat());//bingbing寄生继承类似于寄生构造函数,创建一个不依赖于具体类型的“工厂”函数,专门处理对象的继承过程,然后返回继承的对象实例。幸运的是,我们不需要自己实现这一点,道格拉斯已经为我们提供了一种实现方式:
函数对象(obj){ function F()} { F . prototype=obj;返回新的F();} var SubClass={ name : ' bing ',bra : ' c ' } var SubClass=object(SubClass);console . log(SubCe . name);//bingbing在公共函数中提供一个简单的构造函数,然后将传入的对象实例交给构造函数的原型对象,最后返回构造函数实例,非常简单,但是效果很好,不是吗?这种方式被后人称为“原型继承”,寄生继承是在原型的基础上通过增强对象的自定义属性来实现的:
函数BuildObj(obj){ var o=object(obj);o . SayWhat=function(){ console . log(' hello ');}返回o;} var SupClass={ name : ' bing ',bra : ' c ' } var gf=BuildObj(SupClass);gf . SayWhat();//hello寄生继承也面临着原型中功能复用的问题,于是人们又开始拼积木,于是——寄生组合继承诞生了。目的是解决在指定子类型原型时调用父类型构造函数的问题,同时最大限度地重用函数。基于上述基础的实现如下:
//参数是两个构造函数,函数inherobj(sub,sup){///,实现实例继承,并获取超类型的副本,var proto=object(sup . prototype);//再次指定proto实例的constructor属性proto.constructor=sub//将创建的对象分配给sub.prototype=proto的原型;} function SupClass(){ this . name=' women ';this.bra=['a ',' b '];} superclass . prototype . sayWhat=function(){ console . log(' hello ');} function SubClass(){ this . subname=' your sis ter ';super class . call(this);} inheritObj(子类,超类);var sub 1=new SubClass();console . log(sub 1 . SayWhat());//hello,这个实现避免了超类型的两次调用,省略了SubClass.prototype上不必要的属性,保留了原型链,从而真正结束了继承之旅,这个实现成为了最理想的继承实现!人们对javascript继承的争议仍在继续。一些人提倡面向对象,而另一些人反对在javascript中付出额外的努力来实现面向对象的特性。不管怎样,至少他们学到了更多!