如何用javascript实现继承_为什么组合继承比原型链继承更常用

JavaScript继承本质是复用而非模仿类,通过__proto__链实现属性方法访问;原型链继承因共享引用类型属性存在缺陷,组合继承则用call/apply隔离实例属性、Object.create复用原型方法,兼顾安全与兼容。

继承的本质是复用,不是模仿类

JavaScript 中没有真正的“类继承”,所谓继承,其实是让一个对象能访问另一个对象的属性和方法。原型链继承靠的是 __proto__ 链查找机制:当访问一个对象上不存在的属性时,引擎会顺着 __proto__ 一层层向上找,直到找到或到达 null。这是 JS 原生支持的机制,但直接用它实现“继承”有明显缺陷。

原型链继承的问题:共享引用类型属性

如果父构造函数中有引用类型属性(比如数组、对象),所有子实例都会共享它:

function Parent() {
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayHi = function() { console.log('hi'); };

function Child() {}
Child.prototype = new Parent(); // 关键问题在这里
const c1 = new Child();
const c2 = new Child();
c1.colors.push('green');
console.log(c2.colors); // ['red', 'blue', 'green'] ← 意外被修改了!

原因在于 new Parent() 创建的是一个真实实例,它的 colors 是挂在实例上的,而这个实例成了 Child 的 prototype —— 所有 Child 实例都通过原型链共享它。

组合继承:构造函数 + 原型链,各干各的事

组合继承把“实例属性初始化”和“方法复用”分开处理:

  • 在子构造函数中用 call/apply 调用父构造函数,确保每个实例都有独立的属性副本;
  • 再把父类的原型方法赋给子类原型,实现方法复用。

function Parent(name) {
  this.name = name;
  this.colors = ['red'];
}
Parent.prototype.sayHi = function() { console.log('hi from ' + this.name); };

function Child(name, age) {
  Parent.call(this, name); // ✅ 每个实例独享 name/colors
  this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // ✅ 复用方法,不执行父构造函数
Child.prototype.constructor = Child; // ✅ 修复 constructor 指向

const c1 = new Child('Alice', 10);
const c2 = new Child('Bob', 12);
c1.colors.push('blue');
console.log(c2.colors); // ['red'] ← 正常,不共享

为什么它更常用:安全、清晰、兼容性好

组合继承不依赖 ES6+ 语法,所有浏览器都支持;逻辑明确:构造函数负责“数据隔离”,原型负责“行为复用”。虽然它会在子类原型上多调用一次父构造函数(不影响实例),但相比原型链继承的数据污染风险,这点小代价完全值得。现代开发中虽常用 class extends,但其底层正是组合继承的语法糖。