从设计思想上谈谈“继承”本身的问题
面试速答(30 秒版 TL;DR)
- 继承解决的是“复用”和“多态”,但代价是“强耦合”:子类天然依赖父类的实现细节。
- 常见问题:
- 脆弱的基类问题(Fragile Base Class):父类一改,子类行为可能悄悄变。
- 层级膨胀:继承树越深,理解成本和修改风险越高。
- 错误的 is-a:为了复用代码硬凑继承关系,违反语义。
- LSP(里氏替换原则)破坏:子类不再能在所有地方替代父类,导致“看似可用,实际出 bug”。
- 常见建议:优先“组合优于继承(Composition over Inheritance)”,用组合、委托、策略、函数式组合来复用能力。
心智模型:继承不是“复用”,是“承诺”
一旦 Child extends Parent:
- 子类承诺遵守父类的约定(不只是类型签名,还有隐含语义、时序、副作用等)。
- 父类也“绑架”了子类:父类内部实现变化会扩散到所有子类。
因此很多时候继承不是复用工具,而是架构上的长期承诺。
JS 语境下的现实:更常用“组合 + 委托”
例子:用组合替代继承(能力拆分)
function canLog(target) {
return Object.assign(target, {
log(msg) {
console.log(msg);
},
});
}
function canSerialize(target) {
return Object.assign(target, {
toJSON() {
return JSON.stringify(this);
},
});
}
function createUser(name) {
const user = { name };
canLog(user);
canSerialize(user);
return user;
}
它的好处是:
- 能力粒度更细,不需要为了“复用一小段逻辑”就引入父类大而全的约束。
- 组合更容易做“按需拼装”,避免继承树爆炸。
常见追问
Q1:继承什么时候仍然是好选择?
当且仅当满足:
- 关系确实是稳定的 is-a(语义上是“一个更具体的父类”)
- 父类接口与语义稳定,变更频率低
- 你明确需要多态,并且愿意承担“父类变更波及子类”的成本
Q2:继承的问题在 JS 里更严重吗?
JS 的原型链本身是动态的,运行期可改 prototype/__proto__,再叠加 monkey patch,会让“继承层级”更难推断和调试。因此工程实践更倾向于:
- 更扁平的对象结构
- 更明确的组合边界
- 更少的继承层级
易错点/坑
- 把“代码复用”当成“继承理由”是最常见误区;继承更像“协议复用”,复用的是接口与语义。
- 过早抽象父类往往导致后续需求一变,继承层级反而拖慢迭代。
速记要点(可背诵)
- 继承的收益是复用与多态,代价是强耦合与脆弱。
- 能组合就组合;继承是长期承诺,不是快捷复用。