继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
在js中可以用extend来去实现
1、js中常见的继承方式
- 原型链继承
- 构造函数继承(借助call)
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
2、原型链继承
这个是用extend实现的继承。可以看出new的两个对象,彼此之间的值,没有任何的影响
class Car {
constructor(color, speed) {
this.color = color
this.speed = [1, 2, 3, 4]
// ...
}
}
// // 货车
class Truck extends Car {
constructor(color, speed) {
super(color, speed)
this.Container = true // 货箱
}
}
let dadi = new Truck('白色')
let heid = new Truck('红色')
dadi.speed.push(5)
console.log(dadi.speed)
console.log(heid.speed)
console.log(Truck.prototype)
输出:
然而采用原型链实现继承
的话,就是将父类直接赋值给子类的prototype
对象上
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child.prototype = new Parent();
var s1 = new Child();
var s2 = new Child();
s1.name = 's1';
s1.play.push(4);
console.log(s1.play);
console.log(s2.play);
console.log(s1.name);
console.log(s2.name);
这里其实可以看出来,当同时new出两个对象。
其中一个修改了play
,另外一个对象的play
同时也发生改变了。
这个是因为采用直接赋值的方式,相当于浅拷贝。
父类中若是存在引用类型的话,只能复制对应的地址,因此修改的时候才会发生改变。
这里当修改name
的时候,不发生改变。因为name
是字符串,是原始数据类型。相当于又复制了一个出来,互不影响。
**采用extend实现的继承,就不存在这个问题。**因此这个方法不是最优的。
3、构造函数继承
借助call,Parent.call(this)改变Parent中的this指向。使得Child可以访问到Parent中的属性。
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 child1 = new Child();
let child2 = new Child();
child1.play.push(4);
console.log(child1.play); // 1,2,3,4
console.log(child2.play); // 1,2,3
console.log(child1.getName())// 报错:child1.getName is not a function
相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法。
为什么不能原型的属性以及方法呢?因为这里的原型指向已经丢失了
在Parent构造函数中输出this,可以发现这里的prototype指向的是Object。
4、组合继承
集合了上面两种方法
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'
方式一和方式二的问题都解决了,但是这里Parent执行两次,造成了资源的浪费。
原型式继承(借助Object.create方法)
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); // 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方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。出现的问题其实是跟原型链直接复制出现的错误是一样的。
寄生式继承
缺点跟原型式继承一样。
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);
let person6 = clone(parent5);
console.log(person5); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
person5.friends.push(2);
console.log(person6.getFriends());
寄生组合式继承
是目前来说最优的继承方式:
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
总结
- 原型链继承:new父类,并赋值给子类的原型属性
- 缺点:new出来的对象,其中一个修改引用数据类型,另外一个会跟着变化。 因为两个实例使用的是同一个原型对象,内存空间是共享的。
- 构造函数继承:借助call调用parent函数。
- 缺点:相较于第一种原型链继承方式,父类的引用属性不会被共享。
- 组合继承:组合了前两种方式
- 缺点:person执行了两次,造成了多构造一次的性能开销。
- 原型式继承:借助
Object.create
方式实现普通对象的继承- 缺点: 因为
Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
- 缺点: 因为
- 寄生式继承。缺点跟原型式继承一样。
- 寄生组合式继承 :
采用Object.create+call方式(最优)