Skip to content

继承

概念

说到继承的概念,首先要说一个经典的例子。

先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。

继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。

注意

在 JavaScript 中,当你多次使用 new 关键字创建一个构造函数的实例时,每个实例都有自己独立的实例属性,但它们共享同一个原型对象。这意味着如果你修改了原型对象上的属性,那么所有实例都会受到影响,因为它们都指向同一个原型对象。

ES5 实现继承

原型链继承

js
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 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。

构造函数继承

js
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。

这样写的时候子类能够拿到父类的属性值,并且解决了第一种继承方式的弊端,为啥解决了?因为压根就没继承父类的原型对象 😅。

问题:父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。

组合继承

js
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 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。

js
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 属性,没有进行覆盖,因此输出父对象的属性。

问题:多个实例的引用类型属性指向相同的内存,存在篡改的可能。

寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。

js
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 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

js
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 关键字调用父类的构造函数和方法。

js
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();

Keep Reading, Keep Writing, Keep Coding