2022-06-01 工作3年后回看JS原型和继承

213 阅读4分钟

一转眼,从19年毕业至今,已有三年前端开发经验了,从一开始接触前端处于小白阶段到现在也可以说一个合格的前端工程师了,当时看JS基础有很多不明白的知识,为了应付面试死记硬背,然而现在回过头来在看这些知识,会突然觉得恍然大悟,本篇文章就是重读JS红宝书中的原型以及原型链和继承的知识

什么是JS原型和原型链

红宝书中这样介绍原型:

每个构造函数都有一个属性prototype指向原型对象,原型对象也有一个属性constructor指回构造函数,而实例有一个内部指针__proto__指向原型对象。

如果原型是另一个类型的实例呢?那就意味这这个原型本身有一个内部指针__proto__指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数,这样就在实例和原型之间构造了一条原型链。

仔细阅读这段话,觉得没啥毛病,但是回过头来一看,啥也没看懂。。。

在网上查看了很多资料,发现某位大佬总结的很到位:

JS原型:指的是为其他对象提供共享属性访问的对象,在创建对象时,每个对象都有一个隐式引用指向它的原型对象或者null

JS原型链:原型也是对象,它也有自己的原型,这样就构成了原型链

JS中的几种继承

JS中的继承主要通过原型链来实现的,主要有以下几种继承方式,每种方式都有优缺点

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

原型链继承

将子类的原型指向父类的一个实例

function Parent(name) {
  this.name = name;
}

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

function Child(name) {
  this.name = name;
}

// 继承Parent
Child.prototype = new Parent();

const child1 = new Child("xiaoxu");
const child2 = new Child("fang");
child1.colors.push("new");
console.log(child2.colors);  // [ 'red', 'yellow', 'new' ]

缺点:

  1. 父类构造函数中的引用类型的值会被创建的所有子类实例所共享
  2. 子类在实例化的时候不能给父类构造函数传参

构造函数继承

对于原型链继承的缺点,指出在子类的构造函数中调用一次父类的构造函数

function Parent(name) {
  this.name = name;
  this.colors = ["red", "yellow"];
}

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

function Child(name) {
  // 继承Parent
  Parent.call(this, name);
}

const child1 = new Child("xiaoxu");
const child2 = new Child("fang");
child1.colors.push("new");
console.log(child2.colors); // [ 'red', 'yellow' ]

优点: 弥补了子类实例化不能向父类构造函数传参的问题

缺点:父类构造函数原型上的方法不能复用

组合继承

融合原型继承和构造函数继承的优缺点,基本思路就是:

  • 使用原型链继承原型上的属性和方法
  • 使用构造函数继承实例属性
function Parent(name) {
  this.name = name;
  this.colors = ["red", "yellow"];
}

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

function Child(name) {
  // 构造函数继承
  Parent.call(this, name);
}

// 原型链继承
Child.prototype = new Parent();

const child1 = new Child("xiaoxu");
const child2 = new Child("fang");
child1.colors.push("new");
console.log(child1.colors); // [ 'red', 'yellow', 'new' ]
console.log(child2.colors); // [ 'red', 'yellow' ]

优点:组合继承弥补了原型链继承和构造函数继承的不足,是JS中使用最多的继承模式

缺点:就是子类调用了两次父类的构造函数

原型式继承

即使不定义类型也可以通过原型实现对象之间的信息共享

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

ES5中可以通过以上object函数创建一个临时构造函数,将传入的对象赋值给构造函数的原型,然后在返回一个实例,本质上是对传入对象的一次浅复制

ES6中可以通过Object.create()方法对原型式继承规范化

let person = {
  name: "xiaoxu",
  friends: ["de", "zhen", "hao"]
};

let person1 = Object.create(person);
let person2 = Object.create(person);
person1.friends.push("ying");

console.log(person2.friends); // [ 'de', 'zhen', 'hao', 'ying' ]

优点:原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象之间共享信息的场合

缺点:属性中的引用值始终会在相关对象之间共享,跟使用原型模式是一样的

寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

function createAnother(original) {
  let clone = Object.create(original);
  clone.sayHi = function () {};
  return clone;
}

优点:适合对象,不在乎类型和构造函数的场景

缺点:和构造函数继承类似,其属性方法难以重用

寄生式组合继承

主要是为了解决组合继承的缺点:父类构造函数被调用了两次,一次是在子类构造函数中调用,另外一次是在创建子类的原型时调用

function Person(name) {
  this.name = name;
  this.colors = ["blue"];
}

Person.prototype.getName = function () {
  console.log(this.name);
};

function Child(name) {
  // 原型链继承
  Person.call(this, name);
}

// 寄生式组合继承
Child.prototype = Object.create(Person.prototype);
Child.prototype.constructor = Child;

const child1 = new Child("xi");
const child2 = new Child("fa");

child1.colors.push("red");
console.log(child2.colors); // ['blue']

可以把寄生式组合继承主要方法封装如下:

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

完美调用了一次父类构造函数,是引用类继承的最佳模式