面向对象的特征之一就是继承。大多数面向对象的编程语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于在JavaScript中函数没有签名,所以无法实现接口继承。在JavaScript中主要是通过原型链来实现继承。
基于原型链实现继承
基于原型链实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。在前面我们已经介绍了原型,构造函数和对象实例之间的关系,并详细的分析了它们的内存模型结构。我们通过下面的例子来分析JavaScript基于原型链实现继承的方法。
// 创建父类 function Parent(){ this.parentValue = "Parent"; } // 在父类的原型中添加方法 Parent.prototype.showParentValue = function(){ alert(this.parentValue); } // 创建子类 function Child(){ this.childValue ="Child"; } // 实现继承,让子类Child的原型链指向Parent对象 Child.prototype = new Parent(); // 在子类的原型中添加方法 Child.prototype.showChildValue = function(){ alert(this.childValue); } // 创建子类对象 var c = new Child(); // 子类对象调用继承自父类的方法 c.showParentValue(); // 子类对象调用自己的方法 c.showChildValue();
在上面的代码中,我们首先创建了一个父类Parent
,并在它的原型中添加了showParentValue
方法。
接着我们创建了一个子类Child
,并通过让子类的原型链指向父类来实现继承。
/** 实现继承的关键代码 **/ Child.prototype = new Parent();
接着在子类的原型链中添加showChildValue
方法。然后创建了一个子类对象,这时的子类对象c既可以调用自己的方法,也可以调用继承自父类的方法。
在执行上面的代码之后,子类和父类的内存模型图如下图所示:
从上图我们可以看到,在创建子类的时候,子类的prototype
属性是指向子类的原型对象的。当我们通过Child.prototype = new Parent();
语句让子类的原型指向父类对象的时候,实际上是重写了子类的原型对象。此时在子类的原型对象中会有一个_proto_
属性指向父类的原型对象。而原来的子类原型对象(图中红色区域)实际上已经没有用了。
之后我们在子类原型中添加的方法会被添加到新的子类原型中。同时,父类中的属性也会被写入到新的子类原型中。
当我们创建了子类对象c之后,通过对象c调用了父类的showParentValue
方法。对象c在自己的空间中没有找到这个方法,就会通过_proto_
属性去子类的原型中查找,同样也没有找到这个方法,接着它有通过原型中的_proto_
属性到父类的原型中去查找,这时,showParentValue
方法被找到,并被正确的执行。
上面的过程就是基于原型链实现继承的过程。
完整的基于原型链实现继承的内存模型
在上面的基于原型链实现继承的内存模型中,我们实际上还少描述了一个对象:Object
。所有的引用类型都是继承自Object
,这个继承也是通过原型链来实现的。因此,完整的基于原型链实现继承的内存模型如下图所示:
所有函数的默认原型都是Object
,所以默认原型都会包含一个内部指针指向Object.prototype
。在Object.prototype
中会有内置的hasOwnProperty
、isPrototypeOf
、propertyEmunerable
、toLocaleString
、toString
和valueOf
方法,所以我们自定义的类型都会继承这些方法。
使用原型链进行继承时的注意事项
在使用原型链进行继承的时候要注意以下问题:
- 1、不能在设定了原型链之后再重新为原型链赋值。
- 2、一定要在原型链赋值之后才能添加或者覆盖方法。
对于第一点,我们来看下面的例子:
function Parent(){ this.parentValue = "Parent"; } Parent.prototype.showParentValue = function(){ alert(this.parentValue); } function Child(){ this.childValue ="Child"; } //实现继承,让Child的原型链指向Parent对象 Child.prototype = new Parent(); //下面的操作重写了Child的原型 Child.prototype = { showChildValue:function(){ alert(this.value); } }
在上面的代码中,我们分别创建了一个父类和一个子类,并让子类的原型指向父类对象,实现继承。但是在这之后,我们又通过Child.prototype = {...}
的方式重写了子类的原型,在原型的重写一文中我们已经讲过,重写原型实际上是使子类的原型重新指向一个新的子类原型,这个新的子类原型和父类之间并没有任何的关联关系,所以子类和父类之间此时不再存在继承关系。
对于第二点也很好理解,也来看一个例子:
function Parent(){ this.parentValue = "Parent"; } Parent.prototype.showParentValue = function(){ alert(this.parentValue); } function Child(){ this.childValue ="Child"; } //在实现继承之前为子类在原型中添加方法 Child.prototype.showChildValue = function(){ alert(this.childValue); } //实现继承,让Child的原型链指向Parent对象 Child.prototype = new Parent();
在上面的代码中,我们分别创建了父类和子类。在创建子类之后马上为子类在原型中添加一个方法,然后才让Child的原型链指向Parent对象,实现继承。
这样做的后果是实现继承之后,子类指向的是新的子类原型,而前面添加的方法是放置在原来的原型中的(内存模型图中的红色区域),所以在实现继承之后,子类对象将不再拥有这个方法,因为原来的原型现在已经没有作用了。
方法的覆盖及原型链继承的缺点
如果我们需要实现子类的方法来覆盖父类的方法,只需要在子类的原型中添加与父类同名的方法即可。
/** 覆盖父类中的showParentValue方法 **/ Child.prototype.showParentValue = function(){ alert("Override Parent method"); }
原型链虽然是否强大,可以实现继承,但是原型链也存在一些缺点。原型链继承的缺点主要有:
- 1、使用原型链进行继承最大的缺点是无法从子类中调用父类的构造函数,这样就没有办法把子类中的属性赋值到父类中。
- 2、如果父类中存在引用类型的属性,此时这个引用类型会添加到子类的原型中,当第一个对象修改这个引用之后,其它对象的引用同样会被修改。
原型链和原型在处理引用类型的值的时候存在同样的问题。我们在介绍原型的时候曾经举过一个使用引用类型的例子。在使用原型链时同样会有这个问题。来看下面的例子:
// 创建父类 function Parent(){ this.parentValue = "Parent"; //引用类型的属性 this.friends = ['Leon','Ada']; } // 在父类的原型中添加方法 Parent.prototype.showParentValue = function(){ console.info(this.name+"["+this.friends+"]"); } // 创建子类 function Child(){ this.childValue ="Child"; } // 实现继承,让子类Child的原型链指向Parent对象 Child.prototype = new Parent(); // 在子类的原型中添加方法 Child.prototype.showChildValue = function(){ console.info(this.name+"["+this.friends+"]"); } // 创建子类对象 var person1 = new Child(); person1.name = "Jack"; person1.friends.push('Tom'); var person2 = new Child(); person2.name = "John"; console.info(person2.friends);
在上面的代码中,在父类中有一个引用类型的数组对象属性friends
,在子类实现继承之后,子类对象person1
为它的friends
添加了一个新的朋友,此时,新的朋友是被添加到父类的原型中的,所以在这之后创建的所有新的对象都会有一个新的朋友“Tom”。这就是引用类型属性存在的问题。