对象继承的方法小结

对象,还有一个非常重要的概念就是继承,在我们常用的设计模式中,基本上都会使用到继承的思想,这里就对象的继承做一下分类总结。

对象继承的方法

1:原型链继承

原型链继承的基本思想:利用原型让一个引用类型继承另外一个引用类型的属性和方法。在直白的说就是:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个构造函数的实例呢?显然,此时的原型对象就包含了一个指向另一个原型的指针,进而指向了另一个构造函数。本质就是一层套一层的把构造函数实例化到另外一个构造函数的prototype上去,,还是直接看下例子吧。

首先,先定义一个构造函数A,内含一个内部属性和一个原型链的方法


function A(){
    this.a = "a";
}
A.prototype.getAValue = function(){
    return this.a;
}


定义另外一个构造函数B,并且使得B构造函数原型链继承A构造函数。


function B(){
    this.b = "b";
}

B.prototype = new A(); //把构造函数A,实例化到构造函数B的prototype上,实现原型链的继承。

B.prototype.getBValue = function(){
    return this.b;
}


添加属于B特有的原型方法,之所以原型链继承之后再添加,是因为如果之前添加,会导致原型链断链。关于断链的情况,请参考“原型链断链的原因”,或者自己去查阅一些相关的知识。

下面看下继承之后,对AB分别实例化,实例属性和方法的区别。


var a = new A();
var b = new B();

console.log("a" in a);                     //true ~a对象中,存在a这个方法或属性
console.log(a.hasOwnProperty("a"));        //true ~a方法或者属性,存在于a的实例中
console.log(a.hasOwnProperty("getAValue"));//false ~getAValue方法不存在与a的实例中
console.log(b.hasOwnProperty("a"));        //false ~a属性不存在于b的实例中
console.log("a" in b);                     //true ~a属性,在b对象中是存在的,不是在实例中,所以是在原型链上,所以,b构造函数继承自a构造函数的属性,都是在原型链上,所以呢,注意继承来的,是否为引用类型的值。
console.log("getAValue" in b);             //true ~getAValue方法也是存在于b的原型链上
console.log("b" in b);                     //true ~b属性存在于b对象中
console.log("getBValue" in b);             //true ~getBValue方法存在于b对象中
console.log("toString" in b);              //true ~toString方法,存在于b对象中,说明b构造函数继承了Object对象的方法和属性。


如果你想确认一下,某一个实例中,是否继承了某一个构造函数,可以这样写:


console.log(a instanceof A);        //ture
console.log(a instanceof Object);   //true
console.log(a instanceof B);        //false
console.log(b instanceof A);        //true
console.log(b instanceof B);        //true 
console.log(b instanceof Object);   //true


2:借用构造函数实现继承

在上面的原型链继承中,我们也提到了一点就是:原型链继承来的东西,会把所有的属性和方法都添加到原型链中,这样就有一个问题就是,原型链中的属性值如果是引用类型,那么就会导致该引用类型会被所有的实例共享,这不是我们想要看到的。如果您不了解这个情况,请参考上一篇文章,第6中对象创建方法:原型模式

简单例子:


function A(){
    this.a = ["a1","a2","a3"];
}

function B(){}
B.prototype = new A();

var a = new B(),
    b = new B();
	
a.a.push("a4");
console.log(a.a);      //["a1", "a2", "a3", "a4"]
console.log(b.a);      //["a1", "a2", "a3", "a4"]


这样的结果并不是我们理想的结果,所有为了解决继承中,会出现这个问题的情况,开始使用借用构造函数的技术。这种技术的基本思想就是,在子类构造函数的内部调用超类构造函数。示例代码:


function A(){
    this.a = ["a1","a2","a3"];
}
A.prototype.getValue = function(){
    return this.a;
}
function B(){
    A.call(this);
}
var a = new B(),
    b = new B();
a.a.push("a4");
console.log(a.a);      //["a1", "a2", "a3", "a4"]
console.log(b.a);      //["a1", "a2", "a3"]
console.log(a.getValue());   //error


这个时候,之所以能继承构造函数A内部的方法,是因为改变了this的值,但这里有个缺陷就是,这样的方法,是不是继承构造函数A的原型链上的属性和方法的,例如上述代码中最后一行的代码,console.log(a.getValue());是会抛出错误的,因为并没有能继承这个方法。

这个方法还有一个好处就是,在A的构造函数是有形参输入的话,我们也可以通过A.call(this,name,sex);这样的方法设置,很不错。

由上述两种继承理论及测试也可以想象出一种比较好的继承方法了吧,就像我们在创建对象时,会使用构造函数加原型模式创建,这里,我们也是使用组合模式(借用构造函数和原型链模式进行继承)。

3:组合继承

有时候也叫做伪经典继承,指的是将原型链和借用构造函数两种继承方法组合到一起使用,从而发挥两者各自的优点,主要思想就是,使用原型链实现对原型属性和方法的继承,使用借用构造函数实现对实例属性的继承,而实例中的属性和方法,会覆盖掉原型链中同名的属性和方法。

这样的配合是JS中,最常见的一种继承模式。再想想在创建对象时,使用的构造函数和原型模式的创建,加上这里的组合继承,算是最优的一种组合吧。

看示例吧:


function A(name){
    this.name = name;
    this.a = ["a1","a2","a3"];
}
A.prototype.sayName = function(){
    return this.name;
}
function B(name,age){
    A.call(this,name);
    this.age = age;
}

B.prototype = new A();
B.prototype.sayAge = function(){
    return this.age;
}

var a = new B("zhang",28),
    b = new B("ling",28);

a.a.push("a4");
console.log(a.a);        //["a1", "a2", "a3", "a4"] 
console.log(b.a);        //["a1", "a2", "a3"] 
console.log(a.sayName());//zhang 
console.log(b.sayName());//ling


很不错的实践。

4:原型式继承

这种继承方式,我是怎么也没有理解到,它的优势在哪里?先看代码,之后再把我的一些猜测进行说明。


function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}


这里定义的object的函数,是原型式继承的核心思想所在。从这段函数也可以看出,把继承来的对象放到了原型链上,这里所有继承的属性和方法都是放在了原型链上,而且最初也只有原型链上有继承来的属性,实例中并不会存在。
说的这里,我就很疑问了,这和原型链的继承有什么区别呢,其实它们的思想核心都是一样的,这里在进行继承时,不需要使用构造函数了,不再需要使用new运算符了,我现在也只能想到有这么一个好处,就是避免在继承时,因为忘记使用new运算时导致的错误的可能。

下面就是继承的使用方法:


var person = {
    name:"zhang",
    friend:["andy","lina","luna"]
}
//首先定义一个对象,作为被继承者
var antherPerson = object(person);
//去继承person,并且在继承之后,添加实例内部属性。
antherPerson.name = "ling";
antherPerson.friend.push("lion");

var twoPerson = object(person);
twoPerson.name = "two";
twoPerson.friend.push("QOP");

console.log(person);
console.log(antherPerson.name);         //ling 
console.log(antherPerson.friend);       //["andy", "lina", "luna", "lion", "QOP"] 


这种继承思想的实质还是原型链,所以这里,除了继承时的写法有区别之外,其实没有什么太多的需要注意的地方,只要你能理解原型链的思想,这里何时获取什么值,都是在意料之中的。

例如:name属性会变化是因为,重新赋值操作,就会是在实例中,添加了这个属性,而原型链中name属性的值,依然是原来的值,不会变,只是在取值时,优先考虑实例中的属性。而friend属性没有变化,是因为,friend属性是存在于原型链中,这里的操作只是对这个引用类型进行了修改,并没有进行赋值,比如你这样操作:antherPerson.friend = ["lion"];,这样就会把friend属性进行重新赋值,重新赋值之后,这个属性就会被添加到实例中去,所以就不会影响到其他实例的friend属性的值了。说句我的理解:这个就类似于原型链断链,引用类型都是一个指针,重新定义就会把指针变更,不再会指向原来的地址了。

ES5中,也新增了一个方法,来规范原型式继承,Object.create(),该方法接收两个参数:
第一个参数用作新对象原型的对象,按照上面的例子来说,就是代码中的person对象,用于被继承的对象
第二个参数是可选择的,添加一个新对象定义的额外属性的对象。比如上面代码中的name属性,就可以使用该方法进行添加,具体看下面的例子
当第二个参数没有使用时,create的方法和之前的object方法,是完全一样的,我们只需要把上述代码中的object变化成Object.create即可,有兴趣可以自己试试。

如果有第二个参数呢?第二个参数的格式与Object.defineProperties()方法的第二个属性相同(不知道是什么格式的,还是自己查一下吧,这里不多说),同时第二个参数是被添加到继承的对象的实例中去的。看下面的例子:


var person = {
    name:"zhang",
    friend:["andy","lina","luna"]
}
var anotherPerson = Object.create(person,{
    name:{
	value:"ling",
	wirtable:false
    },
    friend:{
	value:["lion"]
    }
});
console.log(anotherPerson.name);       //ling
console.log(anotherPerson.friend);     //["lion"]
console.log(anotherPerson.hasOwnProperty("friend"));//true
anotherPerson.name = "123";
console.log(anotherPerson.name);       //ling


可以看出一些端倪吧,friend的内容都被改成实例中的内容了,跟我们之前提到的,对原型链上的引用类型重新赋值,是完成的相同的功能。

还有一点要注意一下,这里可以设置一些对象的基本属性,比如我这里设置了name属性的wirtable=false,就是说,这个name属性值,是不可被修改的,这个也是在Object.defineProperty()的时候可以看到的,建议去查一下,所以呢,虽然我又对name属性进行了重新赋值,但是并没有成功,最后取到的值,依然是原来的属性值。

还有一个特例就是,如果create的第一个参数是null的时候呢?这时,对象不继承任何其他对象,连Object对象都不会被继承,所以如果我们把上述的方法修改一下:


var anotherPerson = Object.create(null,{
    name:{
	value:"ling",
	wirtable:false
    },
    friend:{
	value:["lion"]
    }
});
console.log(anotherPerson.name);       //ling
console.log(anotherPerson.friend);     //["lion"]
console.log(anotherPerson.hasOwnProperty("friend"));//报错
anotherPerson.name = "123";
console.log(anotherPerson.name);       //ling


那么该对象就只有namefriend属性,像hasOwnProperty这样继承自Object对象的方法就不会存在了。所以当使用时,浏览器会抛出错误。当然,属性的不可修改的设置,依然成立。

那么如果我想要用这个方法创建一个正常的对象呢(对象的创建方法见:自定义创建对象的几种方法

可以把create的第一个参数设置成Object,或者Object.prototype即可。这两种写法的区别也很明显了,前者是继承完整的Object对象,后者是只继承Object对象原型链中的属性和方法。具体如何使用,就各自按需求吧。

注:create方法,只有在支持ES5的浏览器才支持,低版本浏览器是不支持这个方法的。

5:寄生式继承

在上面所说的原型式继承中,有一个问题我们很容易就会发现,就是对于每一个继承的对象,它只有原型链上的方法和属性,而没有自己的实例,这个时候,对于需要的实例属性,我们只能一个一个的添加,这样代码的可重复利用就降低了,而且这里的实例,我们也只能一个一个的添加,不能使用对象字面量的方法统一添加(这是赋值,会改变引用值的),所以为了解决这个问题,就出现了寄生式继承。寄生式继承的基本思想是基于原型式继承的,请看下面的示例代码:


var person = {
    name:"zhang",
    friend:["andy","lina","luna"]
}
function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

function createAnother(original,name,sex){
    var clone = object(original);
    if(typeof name == "string"){
	clone.name = name;
    }
    if(typeof sex == "string"){
	clone.sex = sex;
    }
    clone.sayName = function(){
	return this.name;
    }
    return clone;
}
var anotherPerson = createAnother(person);


这个时候,我们没有想要实例的内容,所以就使用这样的方法进行,只进行了继承。所以我们看下antherPerson实例的属性值:


console.log(anotherPerson.name);                  //zhang
console.log(anotherPerson.friend);                //["andy", "lina", "luna"]
console.log("name" in anotherPerson)              //true
console.log(anotherPerson.hasOwnProperty("name"));//false


方法和属性都被继承了,但是都是继承在原型链上了。

如果我们这样使用函数呢


var anotherPerson = createAnother(person,"ling","male");
console.log(anotherPerson.name);                  //ling
console.log(anotherPerson.sex);                   //male
console.log("name" in anotherPerson)              //true
console.log(anotherPerson.hasOwnProperty("name"));//true


这样其实就是在createAnother函数内部执行了添加实例的代码,而新添加的name属性也出现在了实例中。

当然,看到之前的createAnother方法,你会不会觉得这个方法很扯,为什么扯就是因为输入的后面的方法,可扩展性也太差了,想要添加什么方法,还得改createAnother函数才行,所以呢,这里我们最好就是把需要添加实例的方法,以一个对象的方法添加进去,就类似于Object.create()方法的第二个变量的格式吧,当然啦,也不需要改的完全一样,只是使用一个对象作为第二参数即可。比如改成下面的逻辑。


function createAnother(original,obj){
    var clone = object(original),
        i;
    for(i in obj){
 	clone[i] = obj[i];
    }
    clone.sayName = function(){
	return this.name;
    }
    return clone;
}
var anotherPerson = createAnother(person,{name:"ling",sex:"male"});

console.log(anotherPerson.name);                  //ling
console.log(anotherPerson.sex);                   //male
console.log("name" in anotherPerson)              //true
console.log(anotherPerson.hasOwnProperty("name"));//true


类似这样,其他没有在该部分的代码,和之前的代码相同。

当然这里的实例添加部分的代码,你也可以封装成一个函数使用,类似这样:


function extend(sub,sup){
    var i;
    for(i in sup){
	sub[i] = sup[i];
    }
}
function createAnother(original,obj){
    var clone = object(original);
    extend(clone,obj);
    clone.sayName = function(){
	return this.name;
    }
    return clone;
}


这样的话,如果你还有其他的地方需要使用这种实例的扩展,就不需要再添加额外的代码了。

6:寄生组合式继承

先说说组合继承,组合继承和组合创建对象,都是我们最常用的模式,不过,对于组合继承,也有其可以改善的地方,组合继承最大的问题就是,无论在什么情况下,都会调用两次被继承的构造函数,一次是继承到原型链上,一次是在构造函数内部(不清楚的可以查看之前组合继承部分)。并且在构造函数内部的方法和属性,都会在原型链上有相同的,而事实上是,原型链上的这部分属性是没有必要的。

所以就出现了这个寄生组合式继承:它的基本思想就是,把实例内部的属性和原型链上的属性分别继承。

  • 首先,组合式继承中,构造函数内部的继承,只继承了实例内部的方法,所以这个地方依然使用相同的方法,实现只继承实例内部的属性。
  • 其次,记得为什么我们使用寄生式继承吗?因为原型式继承方法,会把被继承的对象只继承到原型链中,如果我们被继承的对象只是一个原型链对象呢?那么就可以只把原型链上的属性和方法进行继承了。

其根本方法我们可以进行这样的封装:


function inheritPrototype(sub,sup){
    var proto = object(sup.prototype);

    proto.constructor = sub;
    sub.prototype = proto;
}


其中object方法,和之前的方法一样。

看下这样的示例代码:


function Sup(name){
    this.name = name;
    this.friend = ["andy","lina","luna"];
}

Sup.prototype.sayName = function(){
    return this.name;
}

function Sub(name,age){
    Sup.call(this,name);
    this.age = age;
}

inheritPrototype(Sub,Sup);

Sub.prototype.sayAge = function(){
    return this.age;
}


现在我们把Sub进行实例化,并且由之前的理论可以看出,nameage属性,只会存在于实例中,而sayNamesayAge只会存在于原型链中,下面我们对此进行确认。


var aPerson = new Sub("zhang",28);

console.log(aPerson.hasOwnProperty("name"));     
//实例中存在

delete aPerson.name;

console.log("name" in aPerson);                  
//fasle,说明原型链中不存在,记得一点哦,原型链中的属性,
//是不可以被实例删除的。不知道的话,自己去试试吧。

console.log(!aPerson.hasOwnProperty("sayName") && "sayName" in aPerson); 
//true,说明只存在于原型链中


7:一个疑问的地方

直接看代码:


function Sup(name){
    this.name = name;
    this.friend = ["andy","lina","luna"];
}

Sup.prototype.sayName = function(){
    return this.name;
}

function Sub(name,age){
    Sup.call(this,name);
    this.age = age;
}

Sub.prototype = Sup.prototype;    //这样实现单独继承原型链

Sub.prototype.sayAge = function(){
    return this.age;
}


这种写法依然可以实现只继承原型链上的属性和方法,其获取到的结果,和寄生组合式继承有相同的效果,那么它们有什么区别呢?如果没有区别,这样的写法不是更简单,更明了,(这里的原型链继承,就使用的这个方法)。那又为什么出现寄生组合式继承方法呢?(测试在各浏览器下,现在这种方法都没有问题)。这里,如果您能知道原因,请指教,谢谢!

补充2014-3-3:这上面刚提到的这种继承方法

这个必须要感谢“大众点评网”的一位前端开发工程师,今天在给我面试时,让我明白了,我之前第七部分的这个疑问,它的问题在哪里了。也明白了一些寄生式继承它的优点是在哪了,下面进行说明:

在之前,我们也说过了,为什么原型链上的引用类型,是被所有的实例共享的,如果你不是对实例中的原型链中的引用类型进行了重新赋值,而是直接改变这个引用类型的值(例如:使用pushpop等方法操作数组),那么这个改变会反映在所有的实例中,而这里的根本问题也是出于这个原型链的共享,语言描述总归是有点模糊,还是直接看下代码。


function A(){
    this.name = "zhang";
}

A.prototype.sayName = function(){
    return this.name;
}

function B(){
    A.call(this);
    this.age = 28;
}
B.prototype = A.prototype;
B.prototype.sayAge = function(){
    return this.age?this.age:"no set";
}

var a = new A();
    b = new B();
	
console.log(a.sayName());   //zhang
console.log(a.sayAge());    //no set
console.log(b.sayName());   //zhang
console.log(b.sayAge());    //28


从上面的代码中,我们的目的只是为了,B构造函数,继承A构造函数的方法和实例,但是不管如何,我们都不希望B构造函数的一些操作,会影响到A构造函数的属性和方法,可是,如果只是使用B.prototype = A.prototype;这样的方式进行原型链继承,怎么说呢,这里其实应该不能算是继承,这里只能算是赋值吧,我们也知道,原型链其实质就是指向一个对象的指针,这里就会让B构造函数的原型链,也指向了A原型链的这个对象,也就是说,AB是共用一个原型链的,这就跟一个构造函数的不同实例,共享相同的原型链方法和属性。这也是问题的根源了。

如之前的代码展示,如果这个时候,我继续给B构造函数添加一个方法,那么其实质就是在给A构造函数的原型链添加方法(因为他们指向的是同一个原型链对象),这就是一个很大的问题了,B构造函数影响了A的构造函数,那么如果还有一个C构造函数,也想要去继承A,那这样的写法,就会导致,A,B,C他们所有的原型链方法和属性都是相同的,就会有很多不必要的方法和属性,这完全就是一个构造函数进行了多个实例化而已,这样的继承,就没有了任何意义了。

这样就显示出寄生式继承的优点了,主要原因也是,寄生式继承的核心方法:


function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}


把一个新定义的构造函数,进行实例化返回。这样虽然新的构造函数的指针还是指向的原来被继承的对象的原型链,但是被实例化之后,就不会在影响原来的原型链中的属性和方法了。想想看,一个构造函数定义的多个实例,可以自由的添加原型链的方法,而不会影响到被继承的构造函数的原型链。说个我自己的想法,实例化是不是因为导致了原型链断裂,所以就不会再影响原来的原型链了。

这里,就先说到这里吧,如果再发下问题,再做修正。

对象的继承,在编程语言中,是一个比较重要,也是比较难的点,所以,如果您有新的思想,或者发现文中的错误,或者描述不当,请指教,谢谢!

本文地址:http://www.zhangyunling.com/?p=56

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>