在ECMAScript中,继承通过原型链方式来实现。
理解继承首先要理解什么是原型对象。
原型对象
我们所创建的每一个函数都有一个prototype属性,这个属性指向一个对象,该对象包含了这个特定类型的实例共享的所有属性和方法。并且该对象拥有一个constructor构造器属性,指向构造函数。
通过代码来说明:
const Super=function(){
//声明了一个Super构造函数
console.log('I am Super.')
};
console.log(Super.prototype.constructor===Super); //true
Super的原型对象就是Super.prototype,在控制台中查看Super.prototype对象的属性和方法,可以看到constructor属性指向Super,另外还有一些原型方法:
原型对象、实例和构造函数的关系
刚刚我们只是定义了一个构造函数,现在为Super添加一些原型方法,并通过new来为Super创建一个实例。
Super.prototype.name='superName'
Super.prototype.hello=function(){
console.log('hello '+this.name);
}
const sub=new Super();
console.log(Super.prototype.isPrototypeOf(sub)==true)
console.log(Object.getPrototypeOf(sub)==Super.prototype)
sub.hello();
//输出
I am Super.
true
true
hello superName
可以看到,sub实例已经拥有了Super的原型方法和属性。sub是如何拥有的呢?
我们之前通过new操作符生成了一个Super的实例,在new的过程中,做了这些事:
var newObj={}; //创建一个空对象
newObj.__proto__=obj.prototype; //将空对象链接到原型
var result=obj.call(newObj); //让obj的this指向newObj
if ( typeof result =="object"){
return result;
}else{
return newObj; //返回这个新对象
}
将它封装成一个函数试试:
function createSuper(obj){
var newObj={}; //创建一个空对象
newObj.__proto__=obj.prototype; //将空对象链接到原型
var result=obj.call(newObj);
//让obj的this指向newObj,并且执行了obj构造函数
if ( typeof result =="object"){
return result;
}else{
return newObj; //返回这个新对象
}
}
const sub=createSuper(Super);
sub.hello();
//输出
I am Super.
hello superName
可以看到效果是一样的,sub仍然可以访问到Super.prototype的属性和方法。
__proto__属性相当于sub的内部属性[[prototype]],它指向的是构造函数的prototype,在这里就是Super.prototye。我们执行:
newObj.__proto__=obj.prototype
就获得了Super的原型对象,sub拥有了Super的原型属性和方法。但要清楚这个链接只存在于实例和构造函数的原型对象之间,而不是实例和构造函数之间。
接下来就是执行Super构造函数,获得Super的实例属性和方法。
var result=obj.call(newObj); //这里输出I am Super.
再来看看sub.__proto__属性,看到sub的原型链上有了Super的属性和方法:
原型链和继承
构造函数实现继承:
刚刚我们通过构造函数创建了一个对象,现在我们借用构造函数实现继承。思想是在子类型构造函数的内部调用超类型的构造函数。
const SuperType=function(age){
//声明了一个Super构造函数
console.log('I am Super.');
this.age=age;
};
SuperType.prototype.name='superName';
SuperType.prototype.hello=function(){
console.log('hello '+this.name);
}
const SubType=function(age,colors){
SuperType.call(this,age); //在子类构造函数内部调用超类构造函数
this.colors=colors;
}
创建SubType的实例,并尝试访问实例属性和原型方法:
const instance=new SubType(19,["red","blue","yellow"]);
const instance1 = new SubType(20,["black","green"]);
console.log(instance.age,instance.colors)
console.log(instance1.age,instance1.colors)
console.log(instance.hello());
//输出:
//I am Super.
//I am Super.
//19 [ 'red', 'blue', 'yellow' ]
//20 [ 'black', 'green' ]
//TypeError: instance.hello is not a function
每个实例都有独立的属性,但没有继承到SuperType的原型方法。
构造函数继承的优点和缺点:
- 每个实例都有自己的实例属性,并且可以再子类型构造函数中向超类型传递参数。
- 继承不到原型上的属性和方法。
使用原型链继承:
原理:让SuperType的实例成为SubType的原型,把SuperType的所有属性和方法都给了SubType的原型对象
SubType.prototype = new SuperType()
const SuperType=function(){
//声明了一个Super构造函数
console.log('I am Super.');
this.colors=["red","blue","yellow"];
};
SuperType.prototype.showColors=function(){
console.log('supercolors: '+this.colors);
}
const SubType=function(name){
this.subName=name;
}
SubType.prototype=new SuperType();
const instance1=new SubType("sub1");
const instance2=new SubType("sub2");
console.log(instance1.subName,instance2.subName);
instance1.colors.push("instance1");
instance1.showColors()
instance2.showColors()
//I am Super.
//sub1 sub2
//supercolors: red,blue,yellow,instance1
//supercolors: red,blue,yellow,instance1
SubType的实例可以访问到原型方法,但是SuperType上的colors引用类型属性被共享了。改变instance1的colors导致instance2的colors也改变了。
查看instance1的__proto__:
通过创建SuperType的实例,并将实例赋给了SubType的原型对象上,所以SuperType的原型和实例属性都到了SubType.prototype上。SubType的原型改变了。
然后new一个SubType的instance,instance指向了SubType的原型,SubType的原型又指向SuperType的原型,showColors()方法仍在SuperType.prototype中,但colors位于SubType.prototype中。另外,subName属性位于instance实例中。
instance1.__proto__===SubType.prototype //true
SubType.prototype.__proto__===SuperType.prototype //true
原型链的优点和缺点:
- 能继承到原型属性和方法
- 对于原型中包含的引用类型值,如数组,会被所有实例共享。
- 不能向超类型中传递参数
原型式继承和寄生式继承:
原型式继承不使用构造函数,而是基于已有的对象创建新对象,新对象将已有的对象作为原型。
必须有一个对象作为另一个对象的基础。
function object(o){
function F(){}
//给o创建一个临时的构造函数
F.prototype = o;
return new F();
//再用这个构造函数new一个新对象并返回
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);//将新对象作为原型
var yetAnotherPerson=object(person) ;
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends);
//"Shelby,Court,Van,Rob,Barbie"
原型式继承把原型链直接关联了,子级可以直接访问父级的原型,修改引用属性是会修改到父级。
寄生式继承:
基于原型式继承,创建一个函数,该函数只用于封装继承的过程,并且会在创建新对象后为这个新对象添加新的属性和方法,然后返回这个对象。
function createAnother(original){
var clone = object(original);
//通过调用函数创建一个新对象
clone.sayHi = function(){
//以某种方式来增强这个对象
console.log("hi");
};
return clone;
//返回这个对象
}
这二者都是在没有自定义类型和构造函数的情况下考虑使用的。
组合继承
组合继承将原型链和构造函数结合在一起,借用构造函数继承实例属性,原型链继承原型方法和原型属性。这样既能实现原型上函数的共享,又能让每个实例都拥有自己的实例属性。
const SuperType=function(name){
//声明了一个Super构造函数
console.log('I am Super.');
this.supername=name;
this.colors=["red","blue","yellow"];
};
SuperType.prototype.showName=function(){
console.log('superName: '+this.supername);
}
const SubType=function(name,age){
SuperType.call(this,name)
//继承实例属性
this.subage=age;
}
//第一次调用SuperType
SubType.prototype=new SuperType();//继承原型方法
//第二次调用SuperType
const instance1= new SubType('instance1',19);
//第三次调用SuperType
const instance2= new SubType('instance2',20);
instance1.colors.push('instance1');
instance2.colors.push('instance2');
console.log(instance1.colors);
console.log(instance2.colors);
instance1.showName();
instance2.showName();
//I am Super.
//I am Super.
//I am Super.
//[ 'red', 'blue', 'yellow', 'instance1' ]
//[ 'red', 'blue', 'yellow', 'instance2' ]
//superName: instance1
//superName: instance2
引用类型属性没有被共享了,同时也共享了原型方法。
但注意到调用了三次SuperType构造函数。第二次和第三次调用SuperType()是在继承实例属性时调用了SuperType.call(this,name)。
采用寄生组合式继承:
还记得刚刚的寄生式继承吗?寄生式继承创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,然后返回对象。在寄生组合式继承中,仍然借用构造函数继承属性,而通过原型链的混成形式来继承方法。
寄生组合式继承不用为了指定子类型的原型而调用超类型的构造函数,它只需要超类型原型的一个副本。
function inheritPrototype(subType, superType){
//生成一个superType.prototype的实例对象
var prototype = object(superType.prototype);
//让这个实例的constructor属性指向subType
prototype.constructor = subType;
//将新创建的对象副本赋给子类型的原型
subType.prototype = prototype;
}