你以为继承是写个 extends?在 JS 里,那只是最后一步。真正重要的是你到底懂没懂 prototype 和 this。
✨ 一、继承之前,必须把地基打牢
在 JS 的继承体系里,prototype 是骨架,this 是血肉。
任何继承写法,本质都在做两件事:
💡 继承构造函数属性(this 上的)
this.name
this.age
——每个实例独有。
💡 继承原型属性(prototype 上的)
Animal.prototype.run
Animal.prototype.species
——所有实例共享。
👉 继承 = 私有属性 + 原型方法 两套方案都要搞定。
🥕 二、构造函数式继承(call/apply)
一句话:能拿到“父类的属性”,但完全拿不到“父类的方法”。
function Animal(name, age) {
this.name = name;
this.age = age;
}
function Cat(name, age, color) {
Animal.call(this, name, age); // 借用父类构造函数
this.color = color;
}
✔ 实例属性继承成功
✖ 原型链内容完全继承不到
痛点:
“只继承了父类的 private,不继承父类的 prototype。”
❌ 三、把子类 prototype 指向父类 prototype(千万别)
🔥 三行金句:
- 这是新手最容易写的继承方式。
- 也是整个 JS 继承里最危险的一种。
- 你改子类方法 = 直接改父类方法。
示例:
Cat.prototype = Animal.prototype;
然后一刀把自己坑进去了:
Cat.prototype.jump = ()=>{}
console.log(Animal.prototype.jump); // 真的出现了
因为:
两者指向同一个对象。你改谁都一样。
❌ 这招无论何时何地请保持远离。
🐾 四、new Parent() 继承 —— “能用,但不优雅”
这是许多教程给出的方案:
Cat.prototype = new Animal();
确实解决了“污染父类”的问题。
但同时带来两个副作用:
⚠ 1. 父类构造函数会被白执行一次
如果 Animal 里面有逻辑(发请求、打印日志、初始化资源),都会平白执行。
⚠ 2. 多出一堆没必要的属性
因为 new Animal() 生成的对象,会带上空属性:
this.name // undefined
这些会挂在子类原型上,显得冗余且奇怪。
👉 能用,但不推荐。
🏆 五、最正规、最稳定、最清爽:空对象中介
这是手写继承的官方通关方式,完美解决所有问题。
核心思想只有一句:
继承父类 prototype,但不共享引用,不执行构造函数。
🧠 核心代码
function extend(Parent, Child) {
function F() {}
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
使用:
extend(Animal, Cat);
🟩 优点全家桶:
- 不污染父类 prototype
- 不会执行父类构造函数
- 原型链干净清晰
- 子类 constructor 正常指向自己
👉 这是 ES5 最推荐、最标准的继承写法。
👉 ES6 的 class extends 底层其实就是用这套路子。
🧩 六、一个常见误区:实例属性不会改原型属性
Cat.prototype.type = '猫科';
const c = new Cat();
c.type = 'hello';
console.log(c.type); // hello(实例的)
console.log(Cat.prototype.type); // 猫科(原型的)
原因很简单:
JS 找属性是“从自己开始”,找不到才去原型链上找。
所以实例赋值只是在“遮住”原型,不是修改。
📝 七、大总结
| 继承方式 | 可继承属性 | 可继承方法 | 父类构造是否额外执行 | 会污染父类? | 是否推荐 |
|---|---|---|---|---|---|
| call/apply | ✔ | ✖ | 不会 | 不会 | ⭐ 临时用 |
| prototype = prototype | ✔ | ✔ | 不会 | ✔(最严重) | ❌ 别用 |
| new Parent() | ✔ | ✔ | ✔(会额外执行) | 不会 | ⚠ 能用但不优雅 |
| 空对象中介(圣杯) | ✔ | ✔ | 不会 | 不会 | ⭐⭐⭐⭐⭐ 强推 |
🔥 结语
JS 继承所有花活,本质就是:
call/apply 拿属性、prototype 拿方法、中介函数做隔离。
理解这句话,你就真的懂继承了。