继承是面向对象编程的一个重要部分。使用继承可以更好的复用以前开发的代码,缩短开发周期,提升开发效率。
继承的概念
说一个经典的继承例子。
假设定义一个汽车的类,这个汽车颜色,品牌,样式,轮胎等,这些静态样式被称为这个汽车类的属性,汽车可以跑,跑这个动作被称为这个汽车类的方法,根据这个车的类又可以派生出 “轿车” 和 “货车”,给轿车添加一个后备箱的属性,再给货车添加一个大货箱,这样两个车是属于不同的两个类,但是他们都继承自汽车这个类。
从上面这个例子可以看出,轿车和货车这两个类都是继承自汽车这个类。
继承带来哪些便利呢?
子类通过继承父类的属性和方法,子类获得了与父类同样的属性和方法,因此,子类不在需要重复定义与父类中公有的属性和方法。在子类继承父类的同时,也可以将父类的属性和方法进行重新定义,这样子类重写的方法就会覆盖父类中的方法,这被称为方法的重写。这样就使得子类拥有了和父类不同的属性或者方法。
接下来,我们送 ES5 和 ES6 实现继承的方式去讲解继承。
JavaScript 实现继承的几种方式
1. 原型链继承
原理
实现原理:每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例化对象又包含一个原型对象的指针。让子类构造函数原型对象的指针,指向父类构造函数实例对象的原型对象的指针,实现继承。
简单来说就是,父类实例对象中有proto,是对象,叫原型,子类构造函数中有prototype属性,也是对象,也叫原型,由于原型中的方法是可以互相访问的。因此就是让子类构造函数的原型,指向父类实例对象的原型,就实现了原型链继承。
实现代码:
function Parent() {
this.name = 'Tom';
this.data = [1, 2, 3];
}
function Child() {
this.age = 18;
}
Child.prototype = new Parent();
let child = new Child();
console.log(child);
// 打印台输出
/*
Child
age: 18
__proto__: Parent
data: (3) [1, 2, 3]
name: "Tom"
__proto__: Object
*/
从上面的代码以及输出,可以看到 Child 构造函数,在原型上继承了 Parent 构造函数,并且拥有了它的属性。
缺点
let child1 = new Child();
let child2 = new Child();
child1.data.push(4);
console.log(child1.data);
console.log(child2.data);
// 打印台输出
/*
[1, 2, 3, 4]
[1, 2, 3, 4]
*/
可以看到,只改变了 child1 的data 数组的属性,但是child2 实例对象也跟着改变了。这是由于 两个实例使用的是同一个原型对象。它们的内存是共享的,因此当一个对象发生变化时,另一个也会随着发生变化,这是原型链继承的一个很大缺点。
接下来介绍一种能够避免原型属性共享问题的继承方式。
2. 构造函数继承(call,apply)
原理
通过在子类里调用 call 或者 apply 方法,让子类构造函数直接指向父类构造函数。
实现代码:
function Parent() {
this.name = 'Tom';
this.data = [1, 2, 3];
}
Parent.prototype.say = function() {
console.log(123);
}
function Child() {
Parent.call(this);
this.age = 18;
}
let child1 = new Child();
let child2 = new Child();
child1.data.push(4);
console.log(child1);
console.log(child2);
// 控制台打印输出
/*
child1:
Child {name: "Tom", data: Array(4), age: 18}
age: 18
data: (4) [1, 2, 3, 4]
name: "Tom"
__proto__: Object
child2:
Child {name: "Tom", data: Array(3), age: 18}
age: 18
data: (3) [1, 2, 3]
name: "Tom"
__proto__: Object
*/
从打印台输出,可以看出 Child 通过 call 方法,直接将 Parent 中的属性拿到自己身上,这样就实现了属性的继承。同时我们改变实例化传 child1 的 data 属性的值,child2 中的属性值并没有随着 child1 变化,也解决了原型链继承内存共享的问题。
缺点
Parent 的 原型上的say方法并没有被继承,这是因为 Child 实例化对象产生的 child1 原型还是指向自身。没有有继承 Parent 的原型链。
因此,我们将上面两种方式组合在一起使用,实现继承,下面我们来接着说一下!
3. 组合继承(上面两种继承方式的组合)
function Parent () {
this.name = 'parent'
this.num = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name
}
function Child () {
Parent.call(this)
this.age = 18
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const c1 = new Child()
const c2 = new Child()
c1.num.push(4)
console.log(c1.num); // -> [ 1, 2, 3, 4 ]
console.log(c2.num); // -> [ 1, 2, 3 ]
console.log(c1.getName()); // -> parent
console.log(c2.getName()); // -> parent
由上面的结果可知,通过call,继承没有出现属性的数据共享问题,在原型上继承方法,也可以正常使用。
4. 原型式继承(Object.create)
Object.create
这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。
const parent = {
name: 'tom',
age: 18,
num: [1, 2, 3],
getName: function() {
return this.name;
}
}
const c1 = Object.create(parent)
console.log(c1.name); // tom
c1.num.push(4)
console.log(c1.num); // [ 1, 2, 3, 4 ]
const c2 = Object.create(parent)
console.log(c2.num); // [ 1, 2, 3, 4 ]
console.log(c2.getName()); // tom
由上面的结果可以看出,Object.create
实现对象的继承,与浅拷贝相似,引用类型的数据同样也发生了共享,但是可以实现方法的继承。
5. 寄生组合式继承
前面使用原型链继承的方式,存在着调用父类构造函数的浪费。
接下来的方式在使用Object.create
的基础上,减少构造函数的调用,以达到最优继承。
function clone(parent, child) {
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
function Parent() {
this.name = 'tom',
this.num = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this)
}
clone(Parent, Child)
const c1 = new Child()
const c2 = new Child()
c1.num.push(4)
console.log(c1.num); // [ 1, 2, 3, 4 ]
console.log(c2.num); // [ 1, 2, 3 ]
console.log(c1.getName()); // tom
Object.create
实现方法继承,不会共享的原因是因为函数在调用过程中,谁调用函数,函数的this
就会指向谁所以不存在通用的结果。
6. ES6 的 extends 实现继承
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
}
const child = new Child('tom', 18)
console.log(child); // { name: 'tom', age: 18 }
ES6 extends 继承方式也是采用 寄生组合式 的继承方式。