前言
js中继承是一种编程概念,子类可以继承父类的属性和方法,说到了继承又不得不提及一个概念叫多态:也就是相同的方法,不同的表现形态,子类可以重写父类的方法。
继承的方式
- ES5继承,利用构造函数,原型和原型链(原型链继承,借用构造函数继承,组合继承,原型式继承,寄生式继承,寄生组合式继承)。
- ES6继承,class继承,extends关键字实现继承(写法上清晰直观),ES6规定,子类必须在constructor中调用super(),子类没有自己的this,调用super(),等同于调用父类的构造函数,实例化出一个子对象,子类就得到了自己的this对象,拥有与父类相同的属性和方法实现继承。
- 区别:
- ES5继承:先创建一个独立的子类实例对象,然后再将父类的属性和方法添加上去,也就是“实例在前,继承在后”
- ES6继承:先将父类的属性和方法添加到一个空对象上面,然后再将该对象作为子类的实例,也就是“继承在前,实例在后”
继承的简单实现和各自优缺点
原型链继承
- 构造函数:可以通过
new来新建一个对象的函数 - 原型:通俗点讲就是一个js对象,用来放置一些公共的属性和方法
- 实例:通过构造函数实例化出来的对象
三者之前的关系:每个构造函数都有个有原型对象,每个实例对象都有
__porto__属性指向构造函数的原型,构造函数的原型对象同样也有__proto__属性,也指向对应的构造函数的原型,这种有__proto__连接着原型组成的链式结构,就形成了原型链
function Parent(name){
this.parentName = 'parent-name';
}
Parent.prototype.getParentName = function(){
console.log(this.parentName);
}
function Child(){
this.name = 'child-name'
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.getChildName = function(){
console.log(this.name);
}
let p1 = new Child();
p1.getParentName() // parent-name
p1.getChildName() // child-name
上述例子,是原型链式继承,本质是重写原型对象
- 缺点:
- 公用同一个原型对象,也就是原型的引用地址相同,如果给原型Parent增加一些属性和方法,会导致所有的子实例的属性和方法都发生改变
function Parent(name) {
this.parentName = "parent-name";
this.color = [1, 2];
}
Parent.prototype.getParentName = function () {
console.log(this.color);
};
function Child() {
this.name = "child-name";
}
Child.prototype = new Parent();
let p1 = new Child();
p1.getParentName(); // 【1,2】
let p2 = new Child();
p2.color.push(3);
p2.getParentName(); // 【1,2,3】
p1.getParentName(); // 【1,2,3】
- 没法向构造函数中传递参数
借用构造函数继承
在子类中调用父类的构造函数,俗称对象冒充
function Parent(){
this.color = [1,2];
}
function Child(){
Parent.call(this)
}
let p2 = new Child();
p2.color.push(3);
console.log(p2.color); // [1,2,3]
let p1 = new Child();
console.log(p1.color); // [1,2]
let p3 = new Parent();
console.log(p3.color); // [1,2]
- 缺点:
- 无法继承父类原型上的属性和方法,此方法可以传递参数
function Parent(name){
this.color = [1,2];
}
function Child(){
Parent.call(this,name)
}
组合继承
组合继承是原型链继承和借用构造函数的结合体,其实现思想是利用原型链实现对原型属性和方法的继承,而通过构造函数,实现对实例属性的继承。
function Parent(name){
this.name = name;
this.color = [1,2]
}
Parent.prototype.sayName = function(){
console.log(this.name);
console.log(this.color);
}
function Child(name){
Parent.call(this,name)
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let p1 = new Child('ljz');
p1.sayName(); // 'ljz' [1,2]
p1.color.push(3);
p1.sayName(); // 'ljz' [1,2,3]
let p2 = new Child('ljz1');
p2.sayName(); // 'ljz1' [1,2]
缺点:会调用两次父类构造函数,它是JavaScript中最常用的继承模式
原型式继承
原型式继承和原型链继承有点相似,不过他的思想主要是借助原型基于已有有的对象创建新对象,同时还不必创建自定义类型
function createObj(o){
function F(){}
F.prototype = o;
return new F();
}
其本质就是Object.create(obj,options),options可以重写obj里面的一些属性
- 缺点: 引用类型的属性值都会始终共享,和原型链模式一致
寄生式继承
寄生式继承与原型式继承紧密相关,只不过对原型式继承返回对象,做了一些增强,就是创建一个仅用于继承过程的函数,改函数在内部一某种方式增强该对象,
function createEnhanceObj(o){
let clone = createObj(o);
clone.sayHi = function(){
console.log('增强了');
}
return clone;
}
- 缺点: 使用寄生式继承添加函数,函数复用性差,从而会降低效率
寄生组合式继承
基于组合继承的缺点,思考问题,缺点是调用了两次父类的构造函数,这样会使,子实例的原型,和自身属性上存在一些重复,构造函数继承缺点是没法继承父类原型上的属性和方法,那么我们只需要生成一个父类原型的副本,为副本添加constructor属性指向子类构造函数,最后将这个副本赋值给子类构造函数的原型即可。
function Parent(name){
this.name = name;
}
Parent.prototype.getParentName = function(){
console.log('parent');
}
function Child(name){
this.name = name;
}
function enhanceChild(parent,child){
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
enhanceChild(Parent,Child)
Child.prototype.getChildName = function(){
console.log('child')
}
let p1 = new Child();
p1.getParentName(); // parent
p1.getChildName(); // child
总结
ES5的继承都是围绕着原型,原型链实现继承,多种继承方式各有优缺点,请结合实际场景使用。