关于js类的继承的几种方式以及优缺点

1. 原型链继承

  • 优点:简单易实现;父类方法可以被多个子类实例共享,节省内存。

  • 缺点

    1. 所有子类实例共享父类引用属性,一个子类实例的改变会影响其他所有子类实例
    2. 子类实例不能向父类构造函数传参。
  • 例子:如果父类有一个数组属性,所有子类实例都会共享这个数组,一个实例对数组的修改会反映在所有实例上。

    function Parent() {
        this.colors = ['red', 'blue', 'green'];
    }
    
    function Child() {}
    
    Child.prototype = new Parent();
    
    var child1 = new Child();
    child1.colors.push('black');
    
    var child2 = new Child();
    console.log(child2.colors); // ['red', 'blue', 'green', 'black']
    
  • 子类实例不能向父类构造函数传参

      ```javascript
      function Parent(name) {
          this.name = name;
      }
      
      function Child() {}
      
      Child.prototype = new Parent();
      
      var child = new Child("Child Name");
      console.log(child.name); // 输出: undefined
      ```
    

2. 构造函数继承

  • 优点:可以在子类构造函数中向父类传递参数;避免了引用类型的属性被所有实例共享。
  • 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用。
    • 例子:每个子类实例都会创建自己的 sayName 方法,导致无法共享同一个函数。

      function Parent(name) {
          this.name = name;
          this.sayName = function() {
              console.log(this.name);
          };
      }
      
      function Child(name) {
          Parent.call(this, name);
      }
      
      var child1 = new Child('child1');
      var child2 = new Child('child2');
      
      console.log(child1.sayName === child2.sayName); // false
      

3. 组合继承

  • 优点:结合了原型链和构造函数的优点,是JavaScript中最常用的继承模式。
  • 缺点:调用了两次父类构造函数,可能会造成一定的性能问题。
    • 例子:Parent 构造函数被调用两次,一次是在创建 Child.prototype,另一次是在 Child 构造函数中。

      function Parent(name) {
          this.name = name;
      }
      
      function Child(name, age) {
          Parent.call(this, name); // 第一次调用 Parent
          this.age = age;
      }
      
      Child.prototype = new Parent(); // 第二次调用 Parent
      

4. 寄生组合式继承

  • 优点:可以创建一个增强的对象。
  • 缺点:无法实现函数复用;与原型链继承一样,包含引用类型的属性值始终会被共享。
    • 例子:
// 父类
function Animal(name) {
    this.name = name;
    this.colors = ['white', 'black', 'brown'];
}

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

// 子类
function Dog(name, breed) {
    Animal.call(this, name); // 继承属性
    this.breed = breed;
}

// 寄生组合式继承的核心函数
function inheritPrototype(childClass, parentClass) {
    var prototype = Object.create(parentClass.prototype); // 创建对象
    prototype.constructor = childClass; // 增强对象
    childClass.prototype = prototype; // 指定对象
}

// 应用寄生组合式继承
inheritPrototype(Dog, Animal);

// 新增或重写子类方法
Dog.prototype.sayBreed = function() {
    console.log(this.breed);
};

// 测试寄生组合式继承
var myDog = new Dog("Rex", "Golden Retriever");
myDog.sayName();  // 输出 "Rex"
myDog.sayBreed(); // 输出 "Golden Retriever"

在JavaScript中,指定 prototype.constructor = childClass 是重要的一步,因为它确保了继承过程中保持了构造函数(constructor)的正确指向。这个步骤对于理解和维护对象的原型链是非常关键的。

当你使用 Object.create(superType.prototype) 创建一个新对象时,这个新对象的原型是 superType.prototype。这意味着新对象(即子类的原型对象)的 constructor 属性是继承自 superType.prototype 的,因此它指向的是 superType 而不是 childType。这在逻辑上是不正确的,因为我们希望 childType 的实例的构造器(constructor)指向 childType 本身。

通过设置 prototype.constructor = childClass,我们确保了:

  1. 正确的构造函数引用:任何创建自 childClass 的实例都应该引用 childClass 作为其构造函数。
  2. 保持原型链的完整性:这有助于在继承链中保持清晰的结构,便于理解和调试。

以下是一个简化的例子,展示了为什么需要这个步骤:

function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype);

console.log(Child.prototype.constructor === Parent); // true
// 在这一步,Child的原型的构造函数还指向Parent

Child.prototype.constructor = Child;
console.log(Child.prototype.constructor === Child); // true
// 现在我们更正了constructor属性,使其指向Child

在这个例子中,我们首先创建了一个新的对象,它的原型是 Parent.prototype,但这使得 Child.prototype.constructor 错误地指向 Parent。然后,我们将 Child.prototype.constructor 更正为 Child,以反映实际的继承关系。这样,任何 Child 的实例都会正确地将其 constructor 属性指向 Child

你可能感兴趣的:(javascript,开发语言,ecmascript)