JavaScript 中继承基础和应用

51 阅读6分钟

JavaScript 主要使用基于原型链的继承(Prototypal Inheritance),这与传统的基于类的继承(Class-based Inheritance)不同。ES6 引入了 class 语法糖,但其底层机制仍然是基于原型的。

  1. 核心概念:

    • 原型对象:  每个 JavaScript 对象(除了 null)在创建时都会关联到另一个对象,这个对象就是它的原型对象。对象从其原型对象继承属性和方法。
    • 原型链:  当试图访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 引擎会沿着对象的 __proto__(非标准,但浏览器实现) 或其构造函数的 prototype 属性(标准方式 Object.getPrototypeOf(obj))去查找它的原型对象。如果原型对象上也没有,就继续查找原型对象的原型对象,如此层层向上,直到找到该属性或到达原型链的尽头(null)。这种链式结构就是原型链
    • 构造函数:  使用 new 关键字调用的函数。构造函数通常用于创建特定类型的对象实例。构造函数的 prototype 属性指向一个对象,该对象将成为由该构造函数创建的所有实例的原型对象 (instance.__proto__ === Constructor.prototype)。
  2. ES5 及之前实现继承的主要方式:

    • 原型链继承:  让子类的原型对象 (SubType.prototype) 等于父类的实例 (new SuperType())。核心代码:

      function SuperType() { this.property = true; }
      SuperType.prototype.getSuperValue = function() { return this.property; };
      
      function SubType() { this.subProperty = false; }
      // 关键:继承 SuperType
      SubType.prototype = new SuperType(); // 子类原型指向父类实例
      SubType.prototype.getSubValue = function() { return this.subProperty; };
      
      const instance = new SubType();
      console.log(instance.getSuperValue()); // true (从原型链找到)
      
      • 缺点:

        • 引用类型共享问题:  如果父类实例属性包含引用类型值(如数组、对象),这些引用会被所有子类实例共享。修改一个实例的引用类型属性会影响所有其他实例。
        • 不能向父类构造函数传参:  在创建子类实例时,无法向父类构造函数传递参数 (new SuperType() 发生在定义时,而不是实例化 SubType 时)。
    • 借用构造函数继承:  在子类构造函数内部调用父类构造函数 (SuperType.call(this, args))。核心代码:

      function SuperType(name) { this.name = name; this.colors = ['red', 'blue']; }
      function SubType(name, age) {
          SuperType.call(this, name); // "借用"父类构造函数初始化实例属性
          this.age = age;
      }
      const instance1 = new SubType('Alice', 25);
      instance1.colors.push('green'); // 只影响 instance1
      const instance2 = new SubType('Bob', 30);
      console.log(instance2.colors); // ['red', 'blue'] (不共享)
      
      • 优点:  解决了引用类型共享问题,可以在子类构造函数中向父类构造函数传递参数。
      • 缺点:  方法都在构造函数中定义,无法实现函数复用(每个实例都创建自己的方法副本)。父类原型上的方法对子类实例不可见(因为没涉及原型链)。
    • 组合继承:  结合原型链继承和借用构造函数继承。最常用方式。

      function SuperType(name) {
          this.name = name;
          this.colors = ['red', 'blue'];
      }
      SuperType.prototype.sayName = function() { console.log(this.name); };
      
      function SubType(name, age) {
          // 1. 借用构造函数继承属性
          SuperType.call(this, name); // 第二次调用 SuperType
          this.age = age;
      }
      // 2. 原型链继承方法
      SubType.prototype = new SuperType(); // 第一次调用 SuperType
      SubType.prototype.constructor = SubType; // 修复 constructor 指向
      SubType.prototype.sayAge = function() { console.log(this.age); };
      
      const instance1 = new SubType('Alice', 25);
      instance1.colors.push('green');
      const instance2 = new SubType('Bob', 30);
      console.log(instance2.colors); // ['red', 'blue'] (属性不共享)
      instance1.sayName(); // 'Alice' (方法复用)
      
      • 优点:  解决了引用类型共享问题,实现了方法复用,可以在子类构造函数中向父类构造函数传递参数。
      • 缺点:  父类构造函数被调用了两次(一次创建原型 new SuperType(),一次在子类构造函数中 SuperType.call(this))。导致子类原型 (SubType.prototype) 上有一份父类实例属性,子类实例 (instance) 上也有一份父类实例属性(通过 call 得来),造成了属性冗余。
    • 原型式继承:  基于已有对象创建新对象,无需明确定义构造函数。Object.create() 是标准实现。

      const person = { name: 'Default', friends: ['Shelby', 'Court'] };
      const person1 = Object.create(person);
      person1.name = 'Alice';
      person1.friends.push('Van');
      const person2 = Object.create(person);
      person2.name = 'Bob';
      console.log(person2.friends); // ['Shelby', 'Court', 'Van'] (共享了 friends)
      
      • 适用于从简单对象继承的场景。
      • 缺点:  和原型链继承一样,引用类型值会被共享。
    • 寄生式继承:  创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象(添加方法),最后返回这个对象。常与原型式继承结合。

      function createAnother(original) {
          const clone = Object.create(original); // 原型式继承
          clone.sayHi = function() { console.log('Hi!'); }; // 增强对象
          return clone;
      }
      
      • 缺点:  方法无法复用(每个对象都有自己的方法副本)。
    • 寄生组合式继承:  目前公认最理想的继承范式。解决了组合继承调用两次父类构造函数的问题。核心是使用 Object.create() 来设置子类的原型,避免调用父类构造函数。

      function inheritPrototype(subType, superType) {
          const prototype = Object.create(superType.prototype); // 创建父类原型的副本
          prototype.constructor = subType; // 修复 constructor
          subType.prototype = prototype; // 将副本赋值给子类原型
      }
      
      function SuperType(name) {
          this.name = name;
          this.colors = ['red', 'blue'];
      }
      SuperType.prototype.sayName = function() { console.log(this.name); };
      
      function SubType(name, age) {
          SuperType.call(this, name); // 只调用一次父类构造函数 (继承属性)
          this.age = age;
      }
      inheritPrototype(SubType, SuperType); // 继承方法 (不调用父类构造函数)
      SubType.prototype.sayAge = function() { console.log(this.age); };
      
      • 优点:  只调用一次父类构造函数,效率高。避免了在子类原型上创建不必要的、冗余的属性。原型链保持不变。
  3. ES6 class 继承:

    • ES6 引入了 classconstructorextendssuperstatic 等关键字,提供了一种更接近传统面向对象语言的语法来创建类和实现继承。

    • 底层原理:  class 语法本质上是 ES5 构造函数和原型继承的语法糖。extends 和 super 关键字实现了类似寄生组合式继承的效果。

    • 代码示例:

      class SuperType {
          constructor(name) {
              this.name = name;
              this.colors = ['red', 'blue'];
          }
          sayName() {
              console.log(this.name);
          }
      }
      
      class SubType extends SuperType {
          constructor(name, age) {
              super(name); // 相当于 SuperType.call(this, name)
              this.age = age;
          }
          sayAge() {
              console.log(this.age);
          }
      }
      
      const instance = new SubType('Alice', 25);
      instance.sayName(); // 'Alice'
      instance.sayAge(); // 25
      
    • 优点:  语法简洁清晰,更符合直觉。内置了正确的原型链设置(类似于寄生组合继承)。必须使用 new 调用。类内部定义的方法默认在原型上(可复用)。支持静态方法和 super 关键字调用父类方法。

  4. 面试回答要点:

    • 核心机制:  JavaScript 使用基于原型链的继承
    • 关键概念:  原型对象、原型链 (__proto__ / [[Prototype]] -> Object.getPrototypeOf())、构造函数 (prototype 属性)。
    • ES5 主要方式:  重点掌握组合继承(原理、优缺点)和寄生组合式继承(最优解原理)。
    • ES6 class  本质是语法糖,底层基于原型链。理解 extends 和 super 的作用(super 在构造函数中调用父类构造,在方法中调用父类方法)。明确 class 解决了 ES5 继承中的哪些痛点(语法、冗余属性、必须用 new)。
    • 对比:  能对比不同继承方式的优缺点(特别是引用类型共享、构造函数调用次数、方法复用性)。
  • 总结:  继承在JavaScript 是基于原型链的语言。理解原型对象、原型链、构造函数的关系是基础。ES5 有多种实现方式,组合继承常用但非最优,寄生组合式继承是最佳实践。ES6 class 提供了更优雅的语法糖,其底层机制仍然是原型链,使用 extends 和 super 简化了继承实现。