javascript主要包括类式继承、原型继承、拷贝继承这三种继承方式。
类式继承
大多数面向对象的编程都支持类与类继承的特性,但是javascript是基于原型的编程语言,他并不支持这些特性,只能通过其他方法定义并关联多个相似的对象,如new和instanceof。不过在后来的ES6中新增了一些元素,比如class关键字,但是这个并不意味着javascript中是有类的,class只不过是构造函数的语法糖而已。
类式继承的主要思路是:通过构造函数实例化对象,通过原型链将实例对象关联起来。下面将对类式继承进行详细解释。
【原型链继承】
javascript使用原型链作为实现继承的主要方法,实现本质是重写原型对象,代之以一个新类型的实例。下面的代码中,原来存在于SuperType的实例对象中的属性和方法,现在也存在于SubType.prototype中了。
function Super(){
this.value=true;
}
Super.prototype.getValue=function(){
return this.value;
}
function Sub(){};
//Sub继承了Super
Sub.prototype=new Super();
Sub.prototype.constructor=Sub;
var instance=new Sub();
console.log(intance.getValue());//true;
原型链最主要的问题在于包含引用类型值的原型属性会被所有实例共享,而这也是为什么要在构造函数中,而不是原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。
function Super(){
this.colors = ['red','blue','green'];
}
function Sub(){};
//Sub继承了Super
Sub.prototype = new Super();
var instance1 = new Sub();
instance1.colors.push('black');
console.log(instance1.colors);//'red,blue,green,black'
var instance2 = new Sub();
console.log(instance2.colors);//'red,blue,green,black'
原型链的另一个问题是,在创建子类型的实例的时候,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。再加上包含引用类型值的原型属性会被所以实例共享的问题,在实践中很少会单独使用原型继承。
把下面的案例搞懂,原型继承就拿捏了。
<script type="text/javascript">
function Father () {
this.girls = ["志玲", "凤姐"];
}
function Son () {
this.sin=["hahah"]
}
// 子类的原型对象中就有一个属性 girls ,是个数组
var fa=new Father();
Son.prototype = new Father();
var son1 = new Son();
//给son1的girls属性的数组添加一个元素
son1.girls.push("亦非");
var son2 = new Son();
//这时,发现son2中的girls属性的数组内容也发生了改变
alert(son1.girls); // "志玲", "凤姐", "亦非"
//son可以继承到Father的属性,但Father获取不到son的,比如,打印一个使father的实例fa去访问Son的属性sin,结果为undefined
//father的实例,只能访问到Father中的数据,访问不到更新后的数据,比如,打印一个使father的实例fa去访问father的属性girls,结果为["志玲", "凤姐"],获取不到新新加的“亦菲”
//son的实例son1,对父类的属性更改时,son2也可以获取到,比如,son1对father的girls属性添加了一个“亦菲”,那么打印son2的girls属性时,结果也为"志玲", "凤姐", "亦非"
//son的实例son1,对子类的属性更改时,son2获取不到,比如,son1对son的sin属性添加"lala",那么son2的sin属性的打印结果仍然为“hahah”
///注意:son1对father属性改变的时候只能是更新的时候才能被son2获取到,假如直接将属性的值给替换掉了,那么son2就获取不到了。比如:将 son1.girls.push("亦非"); 改为son1.girls=["亦菲"],这个操作就是将son1的属性直接给替换掉了,(可以理解为给son1添加了一个本地属性girls并设置了相关值)那么当实例中存在和原型对象上同名的属性时,会自动屏蔽原型对象上的同名属性。
</script>
【借用构造函数继承】
借用构造函数的技术(有时候也叫伪类继承或经典继承)。基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数,通过使用apply()或call()方法在新创建的对象上执行构造函数。
function Super(){
this.colors=['red'、'blue'、'green'];
}
function Sub(){
//继承了Super
Super.call(this);
}
var instance1=new Sub();
instance1.colors.push('black');
console.log(instance1.colors);//['red'、'blue'、'green'、'black']
var instance2=new Sub();
console.log(instance2.colors);//['red'、'blue'、'green']
相对于原型链来说,借用构造函数有一个很大的优势,就是可以在子类型构造函数中向超类型构造函数传递参数。
function Super(name){
this.name=name;
}
function Sub(){
//继承了Super,还传了参数
Super.call(this,"call");
}
var instance = new Sub();
console.log(instance.name);//"bai"
console.log(instance.age);//29
但是,如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题----方法都在构造函数中定义,因此函数的复用就无从谈起了。
【组合继承】
组合继承也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function Super(){
this.name=name;
this.colors=['red','blue','green'];
}
Super.prototype.sayName=function(){
console.log(this.name);
}
function Sub(name,age){
//继承属性
Super.call(this,name);
this.age=age;
}
//继承方法
Sub.prototype=new Super();//此时 Sub.prototype 中的 constructor 被重写了,会导致 instance1.constructor === Super
Sub.prototype.constructor=Sub;//将 Sub 原型对象的 constructor 指针重新指向 Sub 本身
Sub.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"
instance1.sayAge();//29
var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"
instance2.sayAge();//27
组合继承有他自己的问题。那就是无论在什么情况下,都会调用两次父类型构造函数:一次是在创建子类型原型的时候,另一次是子类型构造函数内部。子类型最终会包含父类型对象的全部实例属性,但不得不在调用子类型构造函数的时候重写这些属性。
function Super(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
return this.name;
};
function Sub(name,age){
// 第二次调用Super(),Sub.prototype又得到了name和colors两个属性,并对上次得到的属性值进行了覆盖
Super.call(this,name);
this.age = age;
}
//第一次调用Super(),Sub.prototype得到了name和colors两个属性
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
return this.age;
};
【寄生组合继承】
解决两次调用的方法是使用寄生组合式继承。寄生组合式继承与组合继承相似,都是通过借用构造函数来继承不可共享的属性,通过原型链的混成形式来继承方法和可共享的属性。只不过把原型继承的形式变成了寄生式继承。使用寄生组合式继承可以不必为了指定子类型的原型而调用父类型的构造函数,从而寄生式继承只继承了父类型的原型属性,而父类型的实例属性是通过借用构造函数的方式来得到的。
function Super(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Super.prototype.sayName = function(){
return this.name;
};
function Sub(name,age){
Super.call(this,name);
this.age = age;
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"
var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"
这个例子的高效率体现在它只调用了一次Super构造函数,并且因此避免了在Sub.prototype上面创建不必要的、多余的属性。与此同时,原型链还保持不变。
【ES6中的class】
如果使用ES6的class语法,则上面的代码修改如下:
class Super {
constructor(name){
this.name = name;
this.colors = ["red","blue","green"];
}
sayName(){
return this.name;
}
}
class Sub extends Super{
constructor(name,age){
super(name);
this.age = age;
}
}
var instance1 = new Sub("bai",29);
instance1.colors.push("black");
console.log(instance1.colors);//['red','blue','green','black']
instance1.sayName();//"bai"
var instance2 = new Sub("hu",27);
console.log(instance2.colors);//['red','blue','green']
instance2.sayName();//"hu"
ES6的class语法糖隐藏了许多技术细节,在实现同样功能的前提下,代码却优雅不少。