JS基础:继承

151 阅读5分钟

在 JavaScript 中,子类可以通过将其原型对象prototype指向父类的实例或原型,从而继承父类的属性和方法。原型链负责在对象之间建立继承关系。

原型链继承

原型链继承是将子类的原型prototype指向父类的示例实现继承

function Parent() {
  // 原型属性
  this.name = "parent";
  this.colors = ["red", "blue", "green"];
}
// 原型方法
Parent.prototype.sayName = function () {
  console.log(this.name);
}

function Child() {
  this.type = "child";
}
Child.prototype = new Parent()

const child = new Child()
const child2 = new Child();
child2.name = "child2";
console.log(child.name); // parent
console.log(child2.name); // child2
child.sayName(); // parent
child2.sayName(); // child2
console.log(child.type); // child
child.colors.push("yellow");
console.log(child.colors); // ["red", "blue", "green", "yellow"]
console.log(child2.colors); // ["red", "blue", "green", "yellow"]

通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,修改其中一个子类实例的父类引用类型的数据,其他子类实例也会跟着改变。

构造函数继承

构造函数继承是通过在子类原型中调用父类的call方法,然后改变this指向进行继承

function Parent1() {
  this.name = "parent1";
  this.colors = ["red", "blue", "green"];
}
Parent1.prototype.sayName = function () {
  console.log(this.name)
}

function Child1() {
  Parent1.call(this);
  this.type = "child1";
}

const child3 = new Child1();
const child4 = new Child1();
console.log(child3.name); // parent1
// console.log(child3.sayName()); // 报错 Uncaught TypeError: child3.sayName is not a function
child3.colors.push("yellow");
console.log(child3.colors); // ["red", "blue", "green", "yellow"]
console.log(child4.colors); // ["red", "blue", "green"]

通过上述代码可以看出子类可以继承父类的属性,但不能继承父类的方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类实例不会受到影响。

组合继承

组合继承是结合上述两种继承方式集两家之大成进行继承。

function Parent2() {
  this.name = "parent2";
  this.colors = ["red", "blue", "green"];
}

Parent2.prototype.sayName = function () {
  console.log(this.name);
};

function Child2() {
  // 第二次调用 Parent3()
  Parent2.call(this);
  this.type = "child2";
}

// 第一次调用 Parent3()
Child2.prototype = new Parent2();
// 手动挂上构造器,指向自己的构造函数
Child2.prototype.constructor = Child2;

const s1 = new Child2();
const s2 = new Child2();
s1.colors.push("yellow");
console.log(s1.colors); // ["red", "blue", "green", "yellow"]
console.log(s2.colors); // ["red", "blue", "green"]
console.log(s1.sayName()); // 正常输出'parent2'
console.log(s2.sayName()); // 正常输出'parent2'

通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类实例不会受到影响。但是在调用过程中执行了两次父类的构造函数。

原型式继承

let Parent4 = {
  name: "parent4",
  colors: ["red", "blue", "green"],
  getName: function () {
    console.log(this.name);
  }
}

let s3 = Object.create(Parent4);
let s4 = Object.create(Parent4);
s3.name = "child4";
s3.colors.push("yellow");
Parent4.sayHello = function () {
  console.log("hello " + this.name);
}
console.log(s3.colors); // ["red", "blue", "green", "yellow"]
console.log(s4.colors); // ["red", "blue", "green", "yellow"]
s3.getName() // 正常输出'child4'
s4.getName() // 正常输出'parent4'
s3.sayHello() // 正常输出'hello child4'
s4.sayHello() // 正常输出'hello parent4'

通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据会受到影响。

寄生式集成

let Parent5 = {
  name: "parent5",
  colors: ["red", "blue", "green"],
  getName: function () {
    console.log(this.name);
  }
}

function clone(original) {
  let clone = Object.create(original);
  clone.type = "child5";
  clone.sayHello = function () {
    console.log("hello " + this.name);
  }
  return clone;
}

Parent5.sayName = function () {
  console.log(this.name);
}
let s5 = clone(Parent5);
let s6 = clone(Parent5);
s5.name = "child5";
s5.colors.push("yellow");
s5.sayHello() // 正常输出'hello child5'
s6.sayHello() // 正常输出'hello parent5'
s5.sayName() // 正常输出'child5'
s6.sayName() // 正常输出'parent5'
console.log(s5.colors); // ["red", "blue", "green", "yellow"]
console.log(s6.colors); // ["red", "blue", "green", "yellow"]

通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据会受到影响。

寄生组合式继承

function clone2(parent, child) {
  // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}
/**
 * 寄生组合式继承
 * 优点:融合原型链继承和构造函数继承的优点
 * 缺点:
 */
function Parent6() {
  this.name = "parent6";
  this.colors = ["red", "blue", "green"];
}
Parent6.prototype.sayName = function () {
  console.log(this.name);
}

function Child6() {
  Parent6.call(this);
  this.type = "child6";
}

clone2(Parent6, Child6);

Child6.prototype.sayHello = function () { console.log("hello " + this.name) }

const s7 = new Child6();
const s8 = new Child6();

s7.colors.push("yellow");
console.log(s7.colors); // ["red", "blue", "green", "yellow"]
console.log(s8.colors); // ["red", "blue", "green"]
s7.sayName() // 正常输出'parent6'
s8.sayName() // 正常输出'parent6'
s7.sayHello() // 正常输出'hello parent6'
s8.sayHello() // 正常输出'hello parent6'

通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据不会受到影响,这也是目前ES5中实现继承的最优方式。

class 继承

class Parent7 {
  constructor() {
    this.name = "parent7";
    this.colors = ["red", "blue", "green"];
  }
  sayName() {
    console.log(this.name);
  }
}

class Child7 extends Parent7 {
  constructor() {
    super();
    this.type = "child7";
  }
  sayHello() {
    console.log("hello " + this.name);
  }
}

const s9 = new Child7();
const s10 = new Child7();

s9.colors.push("yellow");
console.log(s9.colors); // ["red", "blue", "green", "yellow"]
console.log(s10.colors); // ["red", "blue", "green"]
s9.sayName() // 正常输出'parent7'
s10.sayName() // 正常输出'parent7'
s9.sayHello() // 正常输出'hello parent7'
s10.sayHello() // 正常输出'hello parent7'

从上述代码中看出,ES6的语法中使用class 和 extends 关键字实现继承,是现代 JavaScript 的推荐方式。更符合面向对象编程的习惯,内置支持继承,避免了手动设置原型和修复 constructor

总结

继承方式优点缺点
原型链继承简单,方法在原型上复用引用类型属性共享,构造函数无法传参
构造函数继承不共享引用类型属性,支持传参方法无法复用,每次都要重新定义
组合继承结合了原型链和构造函数继承的优点父类构造函数会被调用两次
原型式继承创建新对象非常简单共享引用类型属性,不能传参
寄生式继承灵活,可以增强对象功能性能不佳,无法复用
寄生组合式继承避免了组合继承的缺点,是 ES5 最优的继承方式实现稍复杂
ES6 class 继承简洁、现代,内置支持继承需要现代浏览器或编译工具支持