JavaScript如何实现继承?

615 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

一、 概述

众所周知,继承在是软件编程不可避免的概念,在JavaScript中有以下几种方式:

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

二、实现方法

原型链继承

Child的原型prototype指向父级Parent的构造函数

看以下例子:

// 父级构造函数
function Parent() {
  this.name = "parent";
  this.play = [1, 2, 3];
}

// 子级构造函数
function Child() {
  this.name = "child";
}

// 子级的原型指向父级的实例
Child.prototype = new Parent();

// 实例化子级
const child1 = new Child();
console.log(child1);

上面代码看似没问题,实际存在潜在问题

// 实例化子级
const child1 = new Child();
const child2 = new Child();

child1.play.push(4);

// 更改child1中的数据,child2也会发生改变
console.log(child1); 
console.log(child2);

改变child1play属性,会发现child2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的

构造函数继承

借助 call调用Parent函数

// 父级构造函数
function Parent() {
  this.name = "parent";
  this.play = [1, 2, 3];
}

// 原型式添加方法
Parent.prototype.getName = function () {
  return this.name;
};

// 子级构造函数
function Child() {
  // 传入this 则Parent的this绑定到Child上
  Parent.call(this);
  this.name = "child";
}

// 实例化子级
const child1 = new Child();
console.log(child1.name); // child
console.log(child1.getName()); // 报错 index.js:21 Uncaught TypeError: child1.getName is not a function

可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

前面我们讲到两种继承方式,各有优缺点。组合继承则将前两种方式继承起来

// 父级构造函数
function Parent() {
  this.name = "parent";
  this.play = [1, 2, 3];
}

Parent.prototype.getName = function () {
  return this.name;
};

// 子级构造函数
function Child() {
  // 第一次调用Parent
  Parent.call(this);
  this.name = "child";
}

// 第二次调用Parent
Child.prototype = new Parent();

// 实例化子级
const child1 = new Child();
const child2 = new Child();

child1.play.push(4);
console.log(child1.play, child2.play); // 互不影响
console.log(child1.getName()); // child

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent 执行了两次,造成了多构造一次的性能开销

原型式继承

这里主要借助Object.create方法实现普通对象的继承

const Parent = {
  name: "parent",
  play: [1, 2, 3],
};

// 使用create
let child1 = Object.create(Parent);
child1.name = "child1";

let child2 = Object.create(Parent);
child2.name = "child2";

child1.play.push(4);
console.log(child1.play);
console.log(child2.play); // 也会改变

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

寄生式继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

const Parent = {
  name: "parent",
  play: [1, 2, 3],
};

function clone(origin) {
  let newObj = Object.create(origin);

  newObj.getName = function () {
    return this.name;
  };

  return newObj;
}
let child1 = clone(Parent);
let child2 = clone(Parent);

child1.play.push(4);
console.log(child1.play);
console.log(child2.play); // 也会改变

其优缺点也很明显,跟上面讲的原型式继承一样,只是添加了其他方法,增强了其他能力

寄生组合式继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

function clone(parent, child) {
  child.prototype = Object.create(parent.prototype);
  child.constructor = child;
}

function Parent() {
  this.name = "parent";
  this.play = [1, 2, 3];
}

function Child() {
  Parent.call(this);
  this.name = "child";
}

// 代替 new Parent()
clone(Parent, Child);

Child.prototype.getName = function () {
  return this.name;
};

const child1 = new Child();
const child2 = new Child();

child1.play.push(4);

console.log(child1.getName()); // child
console.log(child1.play);
console.log(child2.play); // 不会受影响

可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题

利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

三、总结

下面以一张图作为总结:

javascript继承图

通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似