随着面向对象编程的发展,继承也越来越受到关注,实现继承是ES唯一支持的继承方式,主要是通过原型链实现的。
原型链
基本思想就是通过原型继承多个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
基于上面阐述的三者关系中,如果再加上一条,如果此时的原型是另一个类型的实例;那就意味着这个原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样就在实例与原型之间构造了一条原型链。
function GrandParent(){
this.property = true;
}
GrandParent.prototype.getPropertyValue = function() {
return this.property ;
}
function Parent(){
this.subProperty = false;
}
// 继承 GrandParent
Parent.prototype = new GrandParent();
Parent.prototype.getSubProperty = function(){
return this.subProperty;
}
let child = new Parent();
console.log(child.getPropertyValue()); // true
Parent通过创建GrandParent的实例并将其赋值给自己的原型Parent.prototype实现了对GrandParent的继承。这个赋值重写了Parent最初的原型,将其替换为GrandParent的实例。这意味着GrandParent实例能够访问到的属性和方法也会存在于Parent.prototype。后面又创建了Parent的实例并调用了它继承的方法getPropertyValue()方法。
子类有时候需要覆盖父类的方法或者新增父类没有的方法,这些方法必须在原型赋值之后再添加到原型上。
function GrandParent(){
this.property = true;
}
GrandParent.prototype.getPropertyValue = function() {
return this.property ;
}
function Parent(){
this.subProperty = false;
}
// 继承 GrandParent
Parent.prototype = new GrandParent();
// 子类新增加的方法
Parent.prototype.getSubProperty = function(){
return this.subProperty;
}
// 覆盖父类的方法
Parent.prototype.getPropertyValue =function(){
return false;
}
let child = new Parent();
console.log(child.getPropertyValue()); // false
在这里要注意一点,我们都知道原型的属性和方法可以通过对象字面量方式去创建,但这里如果这样去创建的话,这将破坏之前的原型链,因为这相当于重写了原型链。
function GrandParent(){
this.property = true;
}
GrandParent.prototype.getPropertyValue = function() {
return this.property ;
}
function Parent(){
this.subProperty = false;
}
// 继承 GrandParent
Parent.prototype = new GrandParent();
Parent.prototype = {
getSubProperty = function(){
return this.subProperty;
},
getPropertyValue =function(){
return false;
}
}
let child = new Parent();
console.log(child.getPropertyValue()); // 出错!
原型链虽然是实现继承的强大工具,但是它也存在着问题;
第一: 原型中包含的引用值会在所有实例间共享
function Person() {}
Person.prototype = {
name: "Jackson",
age: 30,
arr: [1,2,3]
}
let person1 = new Person();
person1.arr.push(4);
console.log(person1.arr) // 1,2,3,4
let person2 = new Person();
console.log(person2.arr) // 1,2,3,4
第二: 子类型在实例化时不能给父类型的构造函数传递参数。
盗用构造函数
上面说到原型链的继承时,子类型不能给父类型传递参数以及原型包含引用值的问题,为了解决这些问题,就延伸出了盗用构造函数继承的方式。其实现思路就是在子类的构造函数里调用父类构造函数,可以通过使用apply()和call()方法以新创建的对象为上下文执行构造函数。
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
function SubType(){
// 继承 SuperType
SuperType.call(this, "JackSon");
}
let instance = new SubType();
instance.colors.push("black");
console.log(instance.colors) // "red","blue","green","black"
console.log(instance.name) // "JackSon"
let instance1 = new SubType();
console.log(instance.colors) // "red","blue","green"
如此就解决了原型链继承的两个缺点。
但是盗用构造函数的方式去实现继承也存在缺点,主要是构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。子类不能访问父类原型上定义的方法。这和继承的初衷是相悖的。因此盗用构造函数的方式也不能单独去使用。
组合继承
组合继承就是揉和了原型链与盗用构造函数的优点,使用原型链继承原型上的属性和方法,使用盗用构造函数去继承实例属性。这样既可以实现方法重用,也可以让每个实例都有自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
console.log(this.age);
}
let instance1 = new SubType("Jackson", 30);
instance1.colors.push("black");
console.log(instance1.colors) // "red","blue","green","black"
instance1.sayName() // "Jackson"
instance1.sayAge() // 30
let instance2 = new SubType("Marden",40);
console.log(instance2.colors) // "red","blue","green"
instance2.sayName() // "Marden"
instance2.sayAge() // 40
组合继承弥补了原型链和盗用构造函数的不足,是javascript中使用最多的继承模式。
原型式继承,寄生式继承,寄生式组合继承请看 js继承(二)