全面搞定 JavaScript 的继承 & 提升代码复用性

359 阅读5分钟

继承是面向对象编程的一个重要部分。使用继承可以更好的复用以前开发的代码,缩短开发周期,提升开发效率

继承的概念

说一个经典的继承例子。

假设定义一个汽车的类,这个汽车颜色,品牌,样式,轮胎等,这些静态样式被称为这个汽车类的属性,汽车可以跑,跑这个动作被称为这个汽车类的方法,根据这个车的类又可以派生出 “轿车” 和 “货车”,给轿车添加一个后备箱的属性,再给货车添加一个大货箱,这样两个车是属于不同的两个类,但是他们都继承自汽车这个类。

从上面这个例子可以看出,轿车和货车这两个类都是继承自汽车这个类。

继承带来哪些便利呢?

子类通过继承父类的属性和方法,子类获得了与父类同样的属性和方法,因此,子类不在需要重复定义与父类中公有的属性和方法。在子类继承父类的同时,也可以将父类的属性和方法进行重新定义,这样子类重写的方法就会覆盖父类中的方法,这被称为方法的重写。这样就使得子类拥有了和父类不同的属性或者方法。

接下来,我们送 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
*/

原型链继承1.png

从上面的代码以及输出,可以看到 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
*/

call继承.png

从打印台输出,可以看出 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 继承方式也是采用 寄生组合式 的继承方式。