JavaScript 继承那些事

139 阅读4分钟

什么是继承

通过某种方式让一个对象可以访问到另一个对象中的属性方法,我们把这种方式称之为继承

通过定义,可以得到的结论:继承的目的就是对可以某些通用 属性方法共用,以达到减小开销的目的。

什么是好的继承

由于继承的目的是为了减小开销,那么一个好的继承,标准就是:

  1. 继承实现的过程中创建的额外对象越少越好;
  2. 创建出来的对象,副作用越小越好(每种创建方式的影响各不相同)。

1. 原型链继承

本质就是通过原型对象 prototype 对象来实现继承。

function Father() {
  this.money = 10000;
  this.houses = ['house1', 'house2', 'house3'];
}

function Son() {}
Son.prototype = new Father();
// Son.prototype.constructor = Son;

const son1 = new Son();
const son2 = new Son();

son1.houses.push('son1-house');
console.log(son2.houses); // ['house1', 'house2', 'house3', 'son1-house']

原型链继承虽然实现了 属性方法 的共享,但是它是借用了 原型链,那么原型链存在的问题也被继承了:

  1. 原型对象中存在 复杂数据类型 时,当其中一个实例修改了,其他实例也会收到影响(次要),比如 son1houses 中添加了 son1-house,son2 也共享到了修改后的数据;
  2. 如果 Father 需要传参,使用这种继承时,无法给父类传参(主要的问题)。

2. 借用构造函数

为了解决原型链继承的问题,通过构造函数的形式实现继承。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.foodType = ['稻', '黍', '稷', '麦'];
  this.eat = (food) => {
    console.log(`吃了${food}`);
  }
}
Person.prototype.move = () => {
  console.log('go go go...');
}

function Male(name, age) {
  /**
   * 这里是解决原型链问题的关键:
   *  1. 等于每个实例上都 复制 了一份父类的属性,所以自身改变了,不会影响其他实例;
   *  2. 使用 call,父类可以传参
   */
  Person.call(this, name, age);
}

const zs = new Male("张三", 20);
zs.foodType.push('豆');
console.log(zs.foodType); // ['稻', '黍', '稷', '麦', '豆']

const ls = new Male("李四", 18);
console.log(ls.foodType); // ['稻', '黍', '稷', '麦']

// 访问不到父类上的方法
ls.move();  // ls.move is not a function

但是它还是存在它的问题:

  1. 将父类上的属性给每个子类都复制了一份,虽然共享了数据和方法,但是影响性能;
  2. 不能继承父类原型链上的方法。

3. 组合式继承

组合继承就是将 原型继承借用构造函数继承 结合起来。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.foodType = ['稻', '黍', '稷', '麦'];
  this.eat = (food) => {
    console.log(`吃了${food}`);
  }
}
Person.prototype.move = () => {
  console.log('go go go...');
}

function Male(name, age) {
  Person.call(this, name, age);
}

Male.prototype = new Person();
// 重写 constructor 属性,指向自己的构造函数
Male.prototype.constructor = Male;

const zs = new Male("张三", 20);
zs.foodType.push('豆');
console.log(zs.foodType); // ['稻', '黍', '稷', '麦', '豆']

const ls = new Male("李四", 18);
console.log(ls.foodType); // ['稻', '黍', '稷', '麦']
ls.move(); // go go go...

组合继承 相比于原型继承最大的区别就是一行代码 Male.prototype = new Person();

缺点也同样明显:

  1. 父类实例化了两遍;
  2. 父类上的实例,在子类的原型上和子类上都存在,浪费内存。

注意:

组合式继承 算是最优 js 继承的主体,而剩下的继承方式,主要是优化父类的方法和属性,在子类的实例和原型上都存在的问题。

4.原型继承

这种方式,更像是 工厂设计模式 而不是继承方式,它通过空的一个构造函数,将传入的对象当这个构造函数的原型。

function generatorObj(obj) {
  // 创建一个空的构造函数
  function Fn() {}
  Fn.prototype = obj;
  return new Fn();
}

const person = {
  vehicle: ['subway', 'car', 'bicycle'],
  type: 'human'
}

const man = generatorObj(person);
man.name = 'zs';
console.log(man.type); // human

这样的方式实现了对方法和属性的共享,但是无法对新生成的对象添加方法和属性;而且是通过方法,返回一个空对象,也没有对象继承关系。

5.寄生式继承

这就是 原型继承 的加强版,解决了无法给新生成的对象添加属性的缺点。

function generatorObj(obj) {
  // 创建一个空的构造函数
  function Fn() {}
  Fn.prototype = obj;
  return new Fn();
}

function createObj(obj, property) {
  const newObj = generatorObj(obj);
  Object.assign(newObj, property);
  return newObj
}

const person = {
  vehicle: ['subway', 'car', 'bicycle'],
  type: 'human'
}

const man = createObj(person, { name: "zs", age: 20 });
console.log(man.name); // zs

寄生式继承,其实就是 Object.create 函数的实质:

const person = {
  vehicle: ['subway', 'car', 'bicycle'],
  type: 'human'
}

const ls = Object.create(person, {
  name: {
    value: 'ls'
  },
  age: {
    value: 20
  },
})

console.log(ls.name); // ls

这种继承,还是更多的还是像 工厂模式,而不是语言的继承;同时对于继承的关联关系也不明确,一般不使用这种方式来实现继承,但是它可以与 组合继承 结合,实现最优的继承方式。

寄生组合继承

寄生式继承组合继承结合,更进一步的优化继承,减少父类实例化的次数,和存了两遍属性问题。

function Person(name) {
  this.name = name;
  this.pets = ['dog', 'cat'];
}

Person.prototype.move = () => {
  console.log('go...');
}

function Man(name, age) {
  // 这里通过 call,已经将父类中的属性共享
  Person.call(this, name);
  this.age = age;
}

// 切记,传入的式 父类的 prototype ,目的是共享父类的 prottotype
Man.prototype = Object.create(Person.prototype);
Man.prototype.constructor = Man;

const zs = new Man('张三', 20);
console.log(zs);
zs.pets.push('psg');

const ls = new Man('李四', 18);
console.log(ls.pets); // ['dog', 'cat']
ls.move(); // go...

这基本就是 js 继承最主流的方式。

总结

extends.png