组合继承是兼顾属性隔离与方法复用的主流方案,但父类构造函数会被调用两次:一次在子类实例化时通过call初始化实例属性,另一次因误用child.prototype = new parent()导致原型上冗余执行;现代写法用object.create避免后者,仅保留一次有效调用。

组合继承在 JavaScript 中是兼顾属性隔离与方法复用的主流方案,但它并非零成本——核心代价在于父类构造函数被调用两次。理解这一机制及其取舍,才能在真实项目中合理选用或优化。
两次调用父类构造函数:实现逻辑与内存开销
组合继承的“组合”二字,正源于它混合使用了两种继承方式,而关键动作就是两次执行父类构造函数:
- 第一次在子类构造函数内部(Parent.call(this, ...)):为每个子类实例单独初始化父类属性(如
name、friends),确保属性私有、可传参、不共享; - 第二次在设置原型链时(Object.create(Parent.prototype) 的底层过程虽不显式调用,但
Child.prototype = new Parent()这类写法会触发——不过标准组合继承已规避此写法,改用Object.create避免该次执行); - 真正造成冗余的是早期误用写法:
Child.prototype = new Parent(),这会导致父类构造函数在原型上执行一次,生成无用的属性副本(如空friends数组挂在Child.prototype上),既浪费内存,又污染原型。
为什么现代写法只算“一次有效调用”?
当前推荐实现中,原型链绑定使用 Object.create(Parent.prototype),它仅创建一个干净对象,其 [[Prototype]] 指向 Parent.prototype,不会执行 Parent 构造函数。因此:
- 父类属性只在子类实例化时通过
call初始化一次,每个实例独有一份; - 父类方法全部存在于
Parent.prototype上,被所有子类实例共享,无重复创建; - 性能瓶颈不在方法,而在构造函数内创建的引用类型(如数组、对象)——若父类构造函数里做了重操作(如深克隆、网络请求模拟),就会在每次实例化时重复执行。
对比其他继承模式的权衡点
组合继承不是唯一解,它在不同维度上的表现需横向比较:
- vs 原型链继承:避免了引用属性共享和无法传参问题,但多了一次构造函数调用开销(可控);
-
vs 寄生组合继承:后者通过
Object.create完全消除原型上的冗余构造函数执行,是组合继承的轻量升级版,ES6class默认采用此模式; -
vs 类字段 + super()(ES6 class):语法更简洁,语义更清晰,底层仍是寄生组合继承,且引擎做了优化(如 V8 对
super()调用有专门路径),实际性能通常更好; -
vs 纯组合(委托/聚合):不依赖原型链,完全解耦,但需手动转发方法、管理生命周期,代码量上升,失去天然的
instanceof和原型方法查找便利性。
实际项目中的建议
不必为理论上的“两次调用”过度担忧,重点应落在具体场景的合理性判断上:
- 若父类构造函数轻量(只赋值基本类型、简单初始化),组合继承足够高效,也是最易理解、调试友好的方案;
- 若需极致性能或构建基础库,优先采用 ES6
class语法,它隐式使用寄生组合继承,且具备静态分析、类型工具友好等额外优势; - 当父子类关系模糊、或父类频繁变更时,与其强继承,不如拆出可组合的工具类或 Hook(如
useEventEmitter、createValidator),用函数式或依赖注入替代; - 警惕“为继承而继承”:如果子类只覆盖一两个方法,其余全靠父类,且这些方法语义松散,大概率该重构为策略模式 + 组合。











