
第一种是以原型链的方式来实现继承,但是这种方式实现存在的缺点是,
在包含引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱,还有就是在创建子类型的时候不能向超类型传递参数。
function Parent() {
this.type = '赛亚人';
this.skill = ['变身', '气功']
}
function Child() {
this.name = '卡卡罗特';
}
Child.prototype = new Parent();
console.log(new Child());
let goku1=new Child()
let goku2=new Child()
goku1.skill.push('瞬移');
console.log(goku1);
console.log(goku2);
我们在代码里的操作本意是只改变goku1的属性,但是为什么goku2的属性也被改变了呢?因为skill是一个数组,为引用类型,goku1与goku2的skill实际是指向同一块内存空间,因此修改会变得混乱。我们将在接下来的继承方式中解决这个问题。
第二种方式是借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是
无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
function Parent(){
this.type = '赛亚人';
}
Parent.prototype.getType = function () {
return this.type;
}
function Child(){
Parent.call(this);
this.name = '卡卡罗特'
}
let goku = new Child();
console.log(goku); // 正确
console.log(goku.getType()); // 报错
我们发现这种构造函数的继承方式只能继承父类的实例属性和方法,不能继承原型属性或者方法。上述两种方式都各有优缺点,如果我们将两种方式结合一下就产生了组合继承。
第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过
将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
function Parent() {
this.type = '赛亚人';
this.skill = ['变身', '气功']
}
Parent.prototype.getType = function () {
return this.type;
}
function Child() {
// 第二次调用 Parent()
Parent.call(this);
this.name = '卡卡罗特';
}
// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;
let goku1 = new Child();
let goku2 = new Child();
goku1.skill.push('瞬移');
console.log(goku1.skill, goku2.skill); // 不互相影响
console.log(goku1.getType()); // 正常输出'赛亚人'
console.log(goku2.getType()); // 正常输出'赛亚人'
在这里我们可以看出Parent()执行了两次,这必然会带来多余的性能开销,那我们怎么解决这个问题呢?此外,上述的三种方法都是围绕构造函数,那对于普通对象而言,怎么进行继承呢?
第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,
向函数中传入一个对象,然后返回以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单的继承,ES5中定义的Object.create()方法就是原型式继承的实现。缺点与原型链方式相同。
let parent = {
type: "赛亚人",
skill: ["变身", "气功"],
getType: function() {
return this.type;
}
};
let goku1 = Object.create(parent);
goku1.name = "卡卡罗特";
goku1.skill.push("瞬移");
let goku2 = Object.create(parent);
goku2.name = "悟空";
goku2.skill.push("舞空术");
console.log(goku1);
console.log(goku2);
这种修改混乱的情况我们就不再赘述了,和第一种方式是同一个道理。
第五种方式是寄生式继承,寄生式继承的思路是
创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行拓展,最后返回这个对象。这个拓展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时,缺点就是没办法实现函数的复用。
let parent = {
type: "赛亚人",
skill: ["变身", "气功"],
getType: function() {
return this.type;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getSkill = function() {
return this.skill
};
return clone;
}
let goku = clone(parent);
console.log(goku.getType());
console.log(goku.getSkill());
第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例作为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是
使用超类型的原型副本来作为子类型的原型,这样就避免了创建不必要的属性。
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent() {
this.type = '赛亚人';
this.skill = ["变身", "气功"];
}
Parent.prototype.getType = function () {
return this.type;
}
function Child() {
Parent.call(this);
this.name = '卡卡罗特';
}
clone(Parent, Child);
Child.prototype.getName = function () {
return this.name;
}
let goku = new Child();
console.log(goku);
console.log(goku.getName());
console.log(goku.getType());
寄生组合式继承是以上这六种里面最优的继承方式,基本上解决了上述继承方式的各种问题。
