在 JavaScript 中,继承是实现代码复用和构建对象关系的重要概念。本文将讨论原型链继承、构造函数继承以及组合继承等几种常见的继承方式,并提供相应的示例代码,并分析它们的特点、优缺点以及适用场景。

在开始讲解 JavaScript 的继承方式之前,我们先来详细解释一下原型(prototype)、构造函数(constructor)和实例对象(instance)这三个概念。因为只有对它们有清晰的理解,才能更好地理解和应用JavaScript的继承机制。现在我们将分别介绍它们的含义和它们之间的关系。

建议点赞收藏本文章,以便日后复习~~

原型、构造函数、实例对象

【面试常见】JS继承与原型、原型链_父类

在 JavaScript 中,原型(prototype)、构造函数(constructor)和实例对象(instance)是面向对象编程中的重要概念,并且它们之间存在着紧密的关系。

  1. 原型(prototype): 原型是JavaScript中对象之间关联的一种机制。每个JavaScript对象(除了 null 和 undefined)都有一个原型对象,它包含了对象的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有定义该属性或方法,JavaScript引擎会通过原型链向上查找,直到找到对应的属性或方法为止。同理,原型链是由对象的原型对象构成的链式结构,通过这种机制,对象可以继承原型对象的属性和方法。
  2. 构造函数(constructor): 构造函数是用于创建对象的函数。在JavaScript中,我们可以通过定义一个函数并使用 new 关键字来创建对象的实例。构造函数定义了对象的初始状态和行为,并且可以在创建实例时对其进行初始化。构造函数可以包含属性和方法,并且可以使用 this 关键字引用要创建的实例对象。
  3. 实例对象(instance): 实例对象是通过构造函数创建的对象,它具有构造函数定义的属性和方法。每个实例对象都是独立的,它们可以根据需要修改自己的属性值,并且可以调用构造函数中定义的方法。实例对象通过原型链与构造函数的原型对象关联在一起,从而实现属性和方法的继承。
function Animal(name) {  // 构造函数(自带原型对象)
  this.name = name || 'Animal'; // 属性
}

Animal.prototype.eat = function(food) { // 方法
  console.log(this.name + '正在吃:' + food);
};

var animal = new Animal('Tom'); // 实例对象

animal.eat('猫粮')

它们之间的关系如下:

  • 每个构造函数都有一个原型对象(prototype),构造函数的原型对象包含了构造函数定义的属性方法
  • 通过构造函数创建的每个实例对象都有一个内部属性(Prototype),它指向构造函数的原型对象。这个属性在浏览器中通常可以通过 __proto__ 访问(某些浏览器不支持)。
  • 实例对象可以访问构造函数原型对象中的属性和方法。因为它们通过原型链与原型对象关联在一起,如果实例对象访问的属性或方法在自身中找不到,JavaScript引擎会自动沿着原型链向上查找,直到找到对应的属性或方法
  • 当我们修改实例对象的属性时,它会在自身中创建一个与原型对象中同名属性的副本,并在之后的访问中直接使用该副本,而不会影响原型对象中的属性
  • 当我们调用实例对象的方法时,JavaScript引擎会优先在实例对象中查找对应的方法,如果找不到则会继续沿着原型链向上查找,直到找到对应的方法。

什么是JavaScript继承

JavaScript继承是指在前端开发中,使用JavaScript实现对象之间属性和方法的继承关系。继承是面向对象编程的重要概念,它允许我们创建基于现有对象的新对象,并在新对象中拥有原有对象的属性和方法

在JavaScript中,继承是通过原型链来实现的。每个对象都有一个原型对象,它包含对象的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有定义该属性或方法,JavaScript引擎会通过原型链向上查找,直到找到对应的属性或方法为止。这种原型链的查找机制使得对象之间可以共享属性和方法,从而实现继承的效果。

【面试常见】JS继承与原型、原型链_面试_02

除了原型链继承外,JavaScript还提供了其他几种实现继承的方式,如构造函数继承、组合继承等。在开发中,继承通常用于构建对象之间的关系,使得子类可以共享父类的属性和方法,并有能力添加自己的特定属性和方法。这样可以减少重复的代码编写,提高开发效率和代码的可维护性。

接下来我们详细介绍这些继承方式的原理和使用方法,并提供相应的代码示例。

JavaScript继承方式

1、原型链继承

原型链继承是一种基于原型的继承方式,它通过将父类的实例作为子类的原型来实现继承关系。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat() {
  Animal.call(this);
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.name = 'Tom';

var cat = new Cat();

原型链继承的特点包括:

  • 实例既是子类的实例,也是父类的实例,继承关系非常纯粹。
  • 子类可以访问父类新增的原型方法和属性。
  • 实现简单,易于理解和实现。

原型链继承的缺点:

  • 子类无法在构造器中新增属性和方法,只能在实例化后添加。
  • 无法实现多继承。
  • 所有实例共享来自原型对象的属性,包括引用属性。

原型链继承适用于简单的继承关系和单一继承需求的场景。

2、构造继承

构造继承通过在子类构造函数中调用父类构造函数,复制父类的实例属性给子类。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(name) {
  Animal.call(this, name);
}

var cat = new Cat();

构造继承的特点包括:

  • 解决了原型链继承中子类实例共享父类引用属性的问题。
  • 可以在创建子类实例时向父类传递参数。
  • 支持多继承,可以调用多个父类构造函数。

构造继承的缺点:

  • 子类实例并不是父类的实例,只是子类的实例。
  • 无法继承父类的原型属性和方法。
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。

构造继承适用于需要继承实例属性、避免引用属性共享以及多继承的场景。

3、实例继承

实例继承通过为父类实例添加新特性,并将其作为子类实例返回。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(name) {
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

var cat = new Cat();

实例继承的特点包括:

  • 不限制调用方式,无论是new 子类()还是子类(),返回的对象具有相同的效果。

实例继承的缺点:

  • 实例是父类的实例,而不是子类的实例。
  • 不支持多继承。

实例继承适用于灵活的对象创建需求,可以根据不同情况返回不同的实例。

4、拷贝继承

拷贝继承通过复制父类的属性和方法给子类实现继承。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(name) {
  Animal.call(this, name);
}

for (var p in Animal.prototype) {
  if (Animal.prototype.hasOwnProperty(p)) {
    Cat.prototype[p] = Animal.prototype[p];
  }
}

Cat.prototype.constructor = Cat;
var cat = new Cat();

拷贝继承的特点包括:

  • 支持多继承。

拷贝继承的缺点:

  • 效率较低,内存占用高,因为需要拷贝父类的属性。
  • 无法获取父类的不可枚举方法。

拷贝继承适用于多继承的场景,但要注意性能和不可枚举方法的问题。

5、组合继承(原型继承+构造继承)

组合继承结合了原型继承和构造继承的优点,通过调用父类构造函数来继承父类的属性,并将父类实例作为子类原型,实现函数复用。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(name) {
  Animal.call(this, name);
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

var cat = new Cat();

组合继承的特点包括:

  • 继承父类实例属性和方法。
  • 继承父类原型属性和方法。
  • 既是父类的实例,也是子类的实例。

组合继承的缺点:

  • 调用了两次父类构造函数,影响性能。

组合继承适用于大多数场景,是一种常用的继承方式。

6、寄生组合继承

寄生组合继承是对组合继承的优化,通过寄生方式避免了两次调用父类构造函数,从而减少了实例化时的重复操作。具体实现如下:

function Animal(name) {
  this.name = name || 'Animal';
}

Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(name) {
  Animal.call(this, name);
}

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

inheritPrototype(Cat, Animal);

var cat = new Cat();

寄生组合继承的特点包括:

  • 实现简单,堪称完美。

寄生组合继承没有明显的缺点,是一种高效且可靠的继承方式。

结语

我们可以根据具体需求选择适合的继承方式。每种继承方式都有自己的优缺点和适用场景,因此我们应根据实际情况进行选择和灵活运用。

在面试中,JS继承与原型、原型链是常见的话题。如果您认为这篇文章对您有帮助或有价值,请不吝点个赞支持一下。同时也建议收藏本文章,以便日后复习。