用机制视角看 JS 继承:语言设计者是如何构建原型链的?

34 阅读3分钟

你以为继承是写个 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(千万别)

🔥 三行金句:

  1. 这是新手最容易写的继承方式。
  2. 也是整个 JS 继承里最危险的一种。
  3. 你改子类方法 = 直接改父类方法。

示例:

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 拿方法、中介函数做隔离。
理解这句话,你就真的懂继承了。