在面向对象的编程思想中,继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。这里我们总结一些JavaScript中写继承的几种方式,以及它们的优缺点。
类式继承
const Animal = function() {
this.superBreath = true;
this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
return this.superBreath;
}
const Cat = function(name) {
this.name = name;
}
Cat.prototype = new Animal(); //类式继承
const kitty = new Cat("kitty");
console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //true
console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //true
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck");
const tom = new Cat("tom");
console.log(tom.types); //[ 'cat', 'dog', 'snake', 'duck' ]
当我们实例化一个父类时,创建了一个新的对象,将父类构造函数中的方法和属性复制给这个对象,并将这个对象的[[prototype]]属性指向了父类的原型对象。将这个新创建的对象赋值给子类的原型对象,子类的原型就可以访问到父类的原型,还可访问到从父类构造函数中复制的属性和方法。
缺点有两个:第一个是子类的原型对象作为一个被父类实例化的对象,如果父类的构造函数中有引用类型的对象,那么引用类型会被子类的所有实例化对象共享。第二个是因为子类靠实例化父类来实现继承,因此继承操作时无法向父类的构造函数传参,父类构造函数的属性无法初始化。
构造函数继承
const Animal = function() {
this.superBreath = true;
this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
return this.superBreath;
}
const Cat = function(name) {
Animal.call(this); //构造函数继承
this.name = name;
}
const kitty = new Cat("kitty");
console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //false
console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //is not a function
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck");
console.log(kitty.types); //[ 'cat', 'dog', 'snake', 'duck' ]
const tom = new Cat("tom");
console.log(tom.types);//[ 'cat', 'dog', 'snake' ]
Animal.call(this)是构造函数继承的关键,我们在调用Animal时强制把它的this绑定到Cat这个函数调用时的作用域,因此每次实例化子类Cat时,都会将Animal构造函数中的属性与方法拷贝一次。但是这种方法没有涉及到原型链的链接,因此实例化的子类对象无法访问父类原型对象中的方法,要想被子类共享,只能放在父类构造函数中。每次都被拷贝一次,这样违背了代码复用的原则,因此这种方法也不推荐使用。
组合继承
组合使用就很好理解了,它将上面两种继承方式的优点集于一身,但同时也有一个小瑕疵。
const Animal = function() {
this.superBreath = true;
this.types = ['cat', 'dog', 'snake'];
}
Animal.prototype.canBreath = function() {
return this.superBreath;
}
const Cat = function(name) {
Animal.call(this);
this.name = name;
}
Cat.prototype = new Animal();
const kitty = new Cat("kitty");
console.log(kitty instanceof Cat); //true
console.log(kitty instanceof Animal); //true
console.log(kitty.name); //kitty
console.log(kitty.canBreath()); //true
console.log(kitty.types); //[ 'cat', 'dog', 'snake' ]
kitty.types.push("duck");
console.log(kitty.types); //[ 'cat', 'dog', 'snake', 'duck' ]
const tom = new Cat("tom");
console.log(tom.types); //[ 'cat', 'dog', 'snake' ]
Animal.call(this)解决了引用类型共享和不能给父类构造函数传参的缺点,Cat.prototype = new Animal();使得子类实例化对象可以共享父类原型对象中的属性与方法。
它的唯一缺点是调用了两次父类构造函数,因此说它还不完美。
原型式继承
let Animal = {
superBreath: true,
animals: ['cat', 'dog']
}
let Cat = function() {
this.subBreath = true;
this.subAnimals = ['cat', 'dog', 'snakes']
}
Cat.prototype = Object.create(Animal);
Cat.prototype.constructor = Cat;
const kitty = new Cat();
console.log(kitty.superBreath); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
console.log(kitty.subBreath); //true
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes' ]
kitty.subAnimals.push("duck");
kitty.animals.push("elephant");
console.log(kitty.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes', 'duck' ]
const tom = new Cat();
console.log(tom.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(tom.subAnimals); //[ 'cat', 'dog', 'snakes' ]
原型式继承的是对类式继承的一个封装,它的内部构造了一个空的函数,将这个函数的原型对象赋予父对象的值,然后返回一个空函数的实例对象。
function F(){};
F.prototype = o; //o是父对象
return new F();
因此可想而知,它的缺点与类式继承是一样的,父类的引用类型会被共用。只是它构造了一个空函数,开销会小一点。
寄生式继承
let Animal = {
superBreath: true,
animals: ['cat', 'dog']
}
let Cat = function() {
this.subBreath = true;
this.subAnimals = ['cat', 'dog', 'snakes']
}
Cat.prototype = Object.create(Animal, { inherit: { value: "inherit" } });
Cat.prototype.constructor = Cat;
const kitty = new Cat();
console.log(kitty.inherit); //inherit
console.log(kitty.superBreath); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
console.log(kitty.subBreath); //true
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes' ]
kitty.subAnimals.push("duck");
kitty.animals.push("elephant");
console.log(kitty.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(kitty.subAnimals); //[ 'cat', 'dog', 'snakes', 'duck' ]
const tom = new Cat();
console.log(tom.animals); //[ 'cat', 'dog', 'elephant' ]
console.log(tom.subAnimals); //[ 'cat', 'dog', 'snakes' ]
寄生式继承顾名思义,它可以对继承的对象进行扩展,为其增加新的属性和方法。
寄生组合式继承
目前为止,最完美的继承方式:
let Animal = function() {
this.superBreath = true;
this.animals = ['cat', 'dog']
}
Animal.prototype.canBreath = function() {
return this.superBreath;
}
let Cat = function() {
Animal.call(this);
this.subBreath = true;
this.subAnimals = ['cat', 'dog', 'snakes']
}
Cat.prototype = Object.create(Animal.prototype);
console.log(Animal.prototype.constructor == Animal); //true
Cat.prototype.constructor = Cat;
console.log(Cat.prototype.constructor == Cat); //true
const kitty = new Cat();
console.log(kitty instanceof Animal); //true
console.log(kitty.canBreath()); //true
console.log(kitty.animals); //[ 'cat', 'dog' ]
kitty.animals.push("snakes");
console.log(kitty.animals); //[ 'cat', 'dog', 'snakes' ]
const miao = new Cat();
console.log(miao.animals); //[ 'cat', 'dog' ]
我们对比第三种组合继承的方式来看,它用了寄生式与构造函数继承的组合方式,使得父类构造函数少调用了一次。但是使用Object.create()方法还有一个小瑕疵,它需要抛弃默认的Cat.prototype,重新创建了一个对象,不能修改已有的默认对象。因此ES6之后又添加了一个辅助函数Object.setPrototypeOf(),直接修改对象的[[prototype]]关联。
let Animal = function() {
this.superBreath = true;
this.animals = ['cat', 'dog']
}
Animal.prototype.canBreath = function() {
return this.superBreath;
}
let Cat = function() {
Animal.call(this);
this.subBreath = true;
this.subAnimals = ['cat', 'dog', 'snakes']
}
Object.setPrototypeOf(Cat.prototype, Animal.prototype)
console.log(Animal.prototype.constructor == Animal);
console.log(Cat.prototype.constructor == Cat);
const kitty = new Cat();
console.log(kitty instanceof Animal);
console.log(kitty.canBreath());
console.log(kitty.subAnimals);
继承的原型链图谱
es5的组合继承:


图片参考:
https://blog.csdn.net/qq_30282133/article/details/84325742