一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情。
继承
继承可以使得子类具有父类别的各种属性和方法,开发过程中不需要重复的去书写一些已经存在的代码,在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。
JavaScripy常见的继承方式有以下几种:
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链继承
原型链继承是比较常用的继承方式,利用了JS的对象拥有原型的特性,每个构造函数都有一个原型对象,其中原型链主要包括三部分,构造函数、原型和实例,构造函数能通过prototype访问原型对象,原型对象通过construtor访问构造函数,构造函数通过new生成实例。让我们来看下原型链如何实现继承
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child1.prototype = new Parent();
console.log(new Child())
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
原型链继承会有一个比较明显的问题,由于继承的是父级的属性,所以s1和s2中的play属性是从父级继承过来的,相当于s1和s2都有play属性,虽然是已经继承到了,但公用的是同一个父级的属性,当修改其中一个play的时候,更改的是父级的play属性,这样又被继承下来,s1和s2中的play属性都会一起修改
构造函数继承
function Parent(){
this.name = 'parent1';
this.play = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent.call(this);
this.type = 'child'
}
let child = new Child();
let child1 = new Child();
child1.name = 'parent2'
child1.play.push(4);
console.log(child); // Child { name: 'parent1', play: [ 1, 2, 3 ], type: 'child' }
console.log(child1); // Child { name: 'parent2', play: [ 1, 2, 3, 4 ], type: 'child' }
// console.log(child.getName()); // 会报错
从上面代码可以看出,构造函数继承可以解决原型链继承子类共享父类属性的问题,但是构造函数继承也存在自己的问题,比如父类通过原型类设置的方法,子类无法进行继承使用,虽然优化了第一种原型链继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法
组合继承
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
组合继承是将前面俩种原型链继承和构造函数继承的方法结合起来,优化了原型链和构造函数的缺陷,虽然能够优秀的解决的问题,但是也存在自己的小问题,比如上面的代码中,Parent3 执行了两次,造成了多构造一次的性能开销。但也是一个较为合理的继承方式
原型式继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
我们知道Object.create方法实现的是浅拷贝,所以这种方式会有浅拷贝的缺点,我们可以看到parent4这个对象中有原始数据类型和引用数据类型,对我们多个拷贝出来的子类来说,多个子类的引用数据类型会指向同一个内存地址,就会出现同原型链继承方式相似的问题。
寄生式继承
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法 其优缺点也很明显,跟上面讲的原型式继承一样
寄生组合式继承
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
寄生组合式继结合了前面几种继承方式的优缺点,其中组合继承较为完善,只是多次调用了方法,寄生继承的缺点和原型继承相似,寄生组合方式结合了俩种方式的优缺点进行改造,可以说是继承方式中相对较优的继承方式
Class语法糖继承
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
Class继承是通过ES6 中的extends关键字直接实现的继承,但是我们可以用babel工具转换进行查看,extends的继承方法其实也是寄生组合的继承方式,但是从书写方面上看,使用Class的书写方式可以让我们整个代码的过程更加清晰,继承的流程也比较流畅