一天两个JS手写题之---模拟实现JS继承

325 阅读5分钟

JavaScript 中的继承是指一个对象获取另一个对象的属性和方法。继承是面向对象编程中一个非常重要的概念,通过继承,我们可以减少代码的重复,提高代码的可重用性。在 JavaScript 中,继承可以通过原型链实现。本文将手动模拟实现 JavaScript 的继承过程,帮助读者更好地理解原型链和继承的原理。

继承的类型

在 JavaScript 中,继承可以分为以下几种类型:

  1. 原型链继承
  2. 构造函数继承
  3. 组合继承
  4. ES6 class 关键字继承

本文将实现前三种类型的继承。

原型链继承

原型链继承是最简单的一种继承方式,其实现原理非常简单,就是利用子类的原型指向父类的实例来实现继承。代码如下:

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

Parent.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

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

Child.prototype = new Parent();

var child = new Child('Tom');
child.sayName(); // 输出 "My name is Tom"

上面的代码中,我们定义了两个构造函数 ParentChild,并将 Child 的原型指向了一个 Parent 的实例,这样 Child 就可以继承 Parent 的属性和方法了。

原型链继承的缺点是父类的引用属性会被所有子类实例共享,如果一个子类实例修改了这个引用属性,那么其他子类实例也会受到影响。此外,在创建子类实例的时候,不能向父类构造函数传递参数。

构造函数继承

构造函数继承是通过在子类构造函数中调用父类构造函数来实现继承。这种方式相比原型链继承,可以避免父类引用属性被共享的问题。代码如下:

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

Parent.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

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

var child = new Child('Tom');
child.sayName(); // 输出 "My name is Tom"

在上面的代码中,我们定义了两个构造函数 ParentChild,并通过 Parent.call(this, name) 将父类的属性初始化到子类中。这样就可以实现子类继承父类的属性了。

但是,通过构造函数继承,子类只能继承父类的实例,不能继承父类原型。

不过,我们还有一种方法 组合继承

组合继承

接下来我们来实现组合继承,组合继承是将原型链继承和构造函数继承结合起来,既能够保留原型链上的特性,又能够保证每个实例都有自己的属性。具体实现如下:

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

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

function SubType(name, age) {
  SuperType.call(this, name); // 第二次调用 SuperType()
  this.age = age;
}

SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

在这个例子中,我们将 SuperType 构造函数的属性和方法赋值给了 SubType 的实例。在 SubType.prototype 上通过 new SuperType() 调用 SuperType 构造函数的时候,SuperType 的构造函数被执行了一次,使得 SubType 实例的属性被设置了一遍,而 SubType.prototype 上的属性和方法则继承自 SuperType.prototype。最后,将 SubType.prototype.constructor 设置为 SubType,避免 SubType.prototype.constructor 被指向了 SuperType。

虽然组合继承是目前最常用的继承方式,但它也有一些缺点,主要是因为它执行了两次超类型的构造函数:一次在调用 SuperType() 创建子类型的原型时,另一次在子类型构造函数内部。这意味着子类型对象上会存在两份相同的属性和方法,一份在子类型实例上,另一份在子类型的原型上,这可能会影响性能。对于这个问题,可以使用 ES6 的 class 语法糖来解决。

class SuperType {
  constructor(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
  }

  sayName() {
    console.log(this.name);
  }
}

class SubType extends SuperType {
  constructor(name, age) {
    super(name);
    this.age = age;
  }

  sayAge() {
    console.log(this.age);
  }
}

ES6 class 关键字继承

相比于 ES5 ,在 ES6 中,我们可以使用 class 语法糖来简化继承的写法。通过 extends 关键字来继承父类的属性和方法,并使用 super() 在子类构造函数中调用父类构造函数。这种写法使得继承更加直观和易懂,也更加符合开发者的习惯。

综上所述,JavaScript 的继承方式有很多种,每种方式都有各自的优点和缺点。开发者可以根据实际情况选择适合自己的继承方式。

当我们使用ES5的方式实现继承时,每次都需要创建一个新的子类原型对象,这会导致一些不必要的内存浪费,尤其是在创建大量的子类时。ES6中提供了一种更好的方式,即使用classextends关键字来实现继承。

使用ES6的方式实现继承非常简单,只需要使用classextends关键字定义子类,并在子类的构造函数中调用super()函数即可。super()函数会调用父类的构造函数,并将当前子类的this作为参数传递给父类的构造函数,从而实现了子类继承父类的属性和方法。

下面是一个使用ES6的方式实现继承的例子:

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog('Fido', 'Golden Retriever');
dog.speak(); // Output: Fido barks.

在上面的例子中,我们使用class关键字定义了一个Animal类,它有一个构造函数和一个speak()方法。然后我们使用extends关键字定义了一个Dog类,它继承自Animal类,并且有一个构造函数和一个speak()方法。在Dog类的构造函数中,我们调用了super(name),这会调用Animal类的构造函数,并将name参数传递给它。

最后,我们创建了一个Dog类的实例dog,并调用了它的speak()方法,它输出了Fido barks.。由于Dog类继承自Animal类,所以它可以访问Animal类中的speak()方法,但是由于Dog类重写了speak()方法,所以它输出的是Fido barks.

总结一下,使用ES6的方式实现继承非常简单,只需要使用classextends关键字定义子类,并在子类的构造函数中调用super()函数即可。这种方式不仅可以避免ES5中创建大量子类原型对象的问题,而且更加简洁易懂,可以提高代码的可读性。

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