手写继承(各个继承有啥缺点)

0 阅读4分钟

一、继承的本质与核心分类

继承是面向对象编程的核心概念,通过原型链实现属性和方法的复用。在JavaScript中,常见的继承方式可分为原型链继承构造函数继承组合继承等,每种方式各有优缺点。

二、经典继承方式手写实现与缺点

1. 原型链继承(最基础方式)
// 父类
function Parent() {
  this.name = 'parent';
  this.hobbies = ['reading'];
}

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

// 原型链继承
function Child() {}
Child.prototype = new Parent(); // 关键:将子类原型指向父类实例
Child.prototype.constructor = Child; // 修复constructor指向

// 测试
const child1 = new Child();
const child2 = new Child();
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading', 'coding']  共享引用类型属性

缺点

  1. 引用类型属性共享:子类实例共享父类原型中的引用类型属性(如数组、对象),一个实例修改会影响所有实例;
  2. 无法向父类构造函数传参:无法在创建子类实例时向父类传递参数(如new Child('参数')无效);
  3. 原型污染风险:直接修改Child.prototype可能意外覆盖父类原型方法。
2. 构造函数继承(借用构造函数)
function Parent(name) {
  this.name = name;
  this.hobbies = ['reading'];
}

// 构造函数继承
function Child(name, age) {
  Parent.call(this, name); // 关键:在子类中调用父类构造函数
  this.age = age;
}

// 测试
const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading']  引用类型属性独立

缺点

  1. 无法继承父类原型方法:子类只能继承父类构造函数中的属性,无法继承Parent.prototype中的方法(如getName);
  2. 代码复用性差:父类原型方法需在子类中重复定义,违反DRY原则;
  3. 构造函数调用冗余:每次创建子类实例都需重新执行父类构造函数逻辑。
3. 组合继承(原型链+构造函数)
function Parent(name) {
  this.name = name;
  this.hobbies = ['reading'];
}

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

// 组合继承(最常用方式)
function Child(name, age) {
  Parent.call(this, name); // 继承属性
  this.age = age;
}

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

// 测试
const child1 = new Child('小明', 18);
const child2 = new Child('小红', 20);
child1.hobbies.push('coding');
console.log(child2.hobbies); // ['reading']  引用类型独立
console.log(child1.getName()); // '小明'  方法继承正常

缺点

  1. 父类构造函数执行两次
    • 第一次:Child.prototype = new Parent() 时创建父类实例;
    • 第二次:new Child() 时通过Parent.call(this)执行父类构造函数;
    • 导致父类属性在原型和实例中重复存在,浪费内存。
  2. 原型链污染风险:若父类构造函数有副作用(如修改全局变量),会被执行两次。
4. 寄生组合继承(优化组合继承)
function Parent(name) {
  this.name = name;
  this.hobbies = ['reading'];
}

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

// 寄生组合继承(最优方式)
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 优化原型链:避免创建父类实例
function createPrototype(parent) {
  const proto = Object.create(parent.prototype); // 核心:用Object.create创建原型
  proto.constructor = Child;
  return proto;
}

Child.prototype = createPrototype(Parent);

// 测试
const child1 = new Child('小明', 18);
console.log(Child.prototype instanceof Parent); // true  原型链正确
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // true  避免重复执行构造函数

缺点

  1. ES5语法复杂度:需手动实现createPrototype函数,代码量增加;
  2. 浏览器兼容性Object.create在IE9+才支持,低版本需polyfill。
5. ES6类继承(class语法)
class Parent {
  constructor(name) {
    this.name = name;
    this.hobbies = ['reading'];
  }
  
  getName() {
    return this.name;
  }
}

// ES6类继承
class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用super()
    this.age = age;
  }
}

// 测试
const child = new Child('小明', 18);

缺点

  1. 底层仍是组合继承:本质是寄生组合继承的语法糖,仍存在构造函数执行顺序限制(必须先调用super());
  2. 无法完全避开原型链:若父类原型方法修改了this指向,子类可能受影响。

三、各继承方式缺点对比表

继承方式核心缺点内存效率代码复杂度
原型链继承引用类型共享、无法传参、原型污染
构造函数继承无法继承原型方法、代码重复
组合继承父类构造函数执行两次、内存浪费
寄生组合继承ES5语法复杂、低版本浏览器兼容问题
ES6类继承依赖super()执行顺序、底层仍有组合继承痕迹

四、问题

1. 问:为什么组合继承会执行两次父类构造函数?

  • 组合继承通过Child.prototype = new Parent()创建父类实例(第一次执行构造函数),再通过Parent.call(this)在子类构造函数中第二次执行。这会导致父类属性在原型和实例中重复存在,浪费内存。寄生组合继承通过Object.create(parent.prototype)避免了第一次构造函数执行,是更优的方案。
2. 问:ES6类继承中的super有什么作用?
    • super()用于调用父类构造函数,必须在子类constructor中先于this使用;
    • super.方法名用于调用父类原型方法,解决了ES5中Parent.prototype.method.call(this)的繁琐写法;
    • 本质是寄生组合继承的语法糖,底层仍通过原型链实现继承。
3. 问:如何实现一个寄生组合继承?请手写核心代码。
  • function inherit(child, parent) {
      // 1. 创建父类原型的副本
      const proto = Object.create(parent.prototype);
      // 2. 设置子类原型的constructor
      proto.constructor = child;
      // 3. 将子类原型指向副本
      child.prototype = proto;
    }
    
    // 使用示例
    function Parent() { /* ... */ }
    function Child() { /* ... */ }
    inherit(Child, Parent);