继承
概念
说到继承的概念,首先要说一个经典的例子。
先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。
继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。
注意
在 JavaScript 中,当你多次使用 new 关键字创建一个构造函数的实例时,每个实例都有自己独立的实例属性,但它们共享同一个原型对象。这意味着如果你修改了原型对象上的属性,那么所有实例都会受到影响,因为它们都指向同一个原型对象。
ES5 实现继承
原型链继承
function Parent() {
this.name = 'parent';
this.arr = [1, 2, 3];
this.say = function () {
console.log('say');
};
}
Parent.prototype.cb = function () {
console.log('prototype-cb');
};
function Child() {
this.type = 'Child';
}
Child.prototype = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.arr.push(55);
console.log(child1.arr, child2.arr);
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。
虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,明明我只改变了 child1 的 arr 属性,为什么 child2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。
构造函数继承
function Parent() {
this.name = 'parent';
this.arr = [1, 2, 3];
this.say = function () {
console.log('say');
};
}
Parent.prototype.cb = function () {
console.log('prototype-cb');
};
function Child() {
Parent.call(this);
this.type = 'Child';
}
var child = new Child();
console.log(child); //没问题
console.log(child.cb()); //报错
可以看到最后打印的 child 在控制台显示,除了 Child 的属性 type 之外,也继承了 Parent 的属性 name,arr,say。
这样写的时候子类能够拿到父类的属性值,并且解决了第一种继承方式的弊端,为啥解决了?因为压根就没继承父类的原型对象 😅。
问题:父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。
组合继承
function Parent() {
this.name = 'parent';
this.arr = [1, 2, 3];
this.say = function () {
console.log('say');
};
}
Parent.prototype.cb = function () {
console.log('prototype-cb');
};
function Child() {
// 二次调用Parent
Parent.call(this);
this.type = 'Child';
}
//首次调用Parent
Child.prototype = new Parent();
// 手动修改constructor,指回Child
Child.prototype.constructor = Child;
var child1 = new Child();
var child2 = new Child();
child1.arr.push(55);
console.log(child1.arr, child2.arr); //互不影响
console.log(child1.cb()); //正常输出prototype-cb
console.log(child2.cb()); //正常输出prototype-cb
问题:我们可以看到 Parent 执行了两次,第一次是改变 Child 的 prototype 的时候,第二次是通过 call 方法调用 Parent 的时候,那么 Parent 多构造一次就多进行了一次性能开销,这是我们不愿看到的。
原型式继承
ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。
let Parent = {
name: 'Parent',
friends: ['p1', 'p2', 'p3'],
getName: function () {
return this.name;
},
};
let person = Object.create(Parent);
person.name = 'tom';
person.friends.push('jerry');
let person1 = Object.create(Parent);
person1.friends.push('lucy');
console.log(person.name); //tom
console.log(person.name === person.getName()); //true
console.log(person1.name); //Parent
console.log(person.friends); //["p1","p2","p3","jerry","lucy"]
console.log(person1.friends); //["p1","p2","p3","jerry","lucy"]
通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 方法。
第一个结果“tom”,比较容易理解,person 继承了 Parent 的 name 属性,但是在这个基础上又进行了自定义。
第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。
第三个结果“Parent”也比较容易理解,person1 继承了 Parent 的 name 属性,没有进行覆盖,因此输出父对象的属性。
问题:多个实例的引用类型属性指向相同的内存,存在篡改的可能。
寄生式继承
使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
let Parent = {
name: 'Parent',
friends: ['p1', 'p2', 'p3'],
getName: function () {
return this.name;
},
};
function clone(origin) {
let clone = Object.create(origin);
clone.getFriends = function () {
return this.friends;
};
return clone;
}
let person = clone(Parent);
let person1 = clone(Parent);
console.log(person.getName());
console.log(person.getFriends());
person.friends.push('我在两个实例上都被加进去了');
console.log(person.friends); //['p1', 'p2', 'p3','我在两个实例上都被加进去了']
// person1 此时的friends 属性本不该有此值('我在两个实例上都被加进去了')
console.log(person1.friends); //['p1', 'p2', 'p3','我在两个实例上都被加进去了']
person 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法。person 通过 clone 的方法,增加了 getFriends 的方法,从而使 person 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承。
问题:优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
寄生组合式继承
结合原型式继承中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式
function clone(parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent(name) {
this.name = name;
this.arr = [1, 2, 3];
this.say = function () {
console.log('say');
};
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name) {
Parent.call(this, name);
this.type = 'child';
}
clone(Parent, Child);
Child.prototype.getType = function () {
return this.type;
};
var childInstance = new Child('父类');
console.log(childInstance.getName()); //父类
console.log(childInstance.getType()); //child
寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。
ES6 实现继承
ES6 的继承方式,通过 extends 关键字实现,通过 super 关键字调用父类的构造函数和方法。
class Person {
constructor(name, gender) {
this.name = name;
this.gender = gender;
}
eat() {
console.log('都会吃');
}
}
// Chinese继承了Person extends继承语法
class Chinese extends Person {
constructor(name, gender) {
// super 代表父类的构造函数
super(name, gender);
this.skin = 'yellow';
}
pingpong() {
console.log('打乒乓球');
}
}
const c1 = new Chinese('王五', 18);
console.log(c1);
c1.eat();
c1.pingpong();