就算现在大家都用 class 写继承,只要你打开 Vue 2/3、React、jQuery、Element-Plus、Ant Design 等任意主流框架源码,底层实现继承的地方,十有八九还是寄生组合式继承。
今天我们就用你自己的笔记和代码,一步一步把原型继承的所有坑和最优解全部讲透。
一、第一步:构造函数继承 —— call 与 apply 的真正用途
JavaScript
function Animal(name, age) {
this.name = name;
this.age = age;
}
function Cat(name, age, color) {
// 关键:把 Animal 的构造函数“借”过来执行,this 指向当前 Cat 实例
Animal.call(this, name, age);
// Animal.apply(this, [name, age]); // 两者完全等价
this.color = color;
}
const cat = new Cat('小白', 1, '黑色');
console.log(cat);
// {name: '小白', age: 1, color: '黑色'}
优点:实例属性完美继承,互不干扰 缺点:只能拿到实例属性,Animal.prototype 上的方法完全拿不到
二、最大雷区:直接赋值原型(你第一个例子的大坑)
你第一个 HTML 里就是经典错误写法:
JavaScript
Cat.prototype = Animal.prototype; // 直接指向同一个对象
Cat.prototype.constructor = Cat;
看起来 cat.species 能拿到了,但只要你在子类加个方法:
JavaScript
Cat.prototype.eat = function() { console.log('吃鱼'); };
灾难发生:
JavaScript
const animal = new Animal('大黄', 5);
animal.eat(); // 竟然也输出了 “吃鱼”!!
这就是原型污染,父类被你不小心改了,生产环境绝对不允许,面试直接红灯。
三、正确姿势:空对象做中介(你第二个和第四个例子已经非常接近了)
我们来把你第二个 HTML 里的代码稍作整理,就是业界经典写法:
JavaScript
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.species = '动物';
function Cat(name, age, color) {
Animal.call(this, name, age);
this.color = color;
}
// 核心:用一个空函数做中介
function inheritPrototype(Child, Parent) {
const F = function() {}; // 中介函数
F.prototype = Parent.prototype; // 借原型
Child.prototype = new F(); // 实例化得到一个干净对象
Child.prototype.constructor = Child; // 修复 constructor
}
// 执行继承
inheritPrototype(Cat, Animal);
// 现在可以安全地在子类原型上添加方法
Cat.prototype.eat = function() {
console.log('eat fish');
};
const cat = new Cat('小白', 1, '黑色');
console.log(cat.species); // '动物'
cat.eat(); // 'eat fish'
// 验证父类原型没被污染
const animal = new Animal('大黄', 5);
console.log(animal.eat); // undefined → 安全!
这就是大名鼎鼎的寄生式继承 + 组合继承的核心实现,jQuery、Vue 2、Element 源码里都用过类似手法。
四、组合继承的唯一小缺陷(很多人忽略)
上面的 inheritPrototype 虽然完美,但有一个极小缺陷:
在 new F() 这一步,实际上又执行了一次 Parent 的构造函数(虽然里面没代码),所以整个继承过程父类构造函数被执行了两次:
- new F() 时一次(虽然没参数)
- Cat 构造函数里 Animal.call(this, ...) 一次
99.99% 的业务场景完全可以忽略,但在构造函数有副作用(比如发网络请求、修改全局状态)时就会出大问题。
五、终极最优解:寄生组合式继承(Holy Grail 圣杯模式)
JavaScript
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.getName = function() {
return this.name;
};
function Cat(name, age, color) {
Animal.call(this, name, age); // 只执行这一次
this.color = color;
}
// 最完美的继承方式
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复 constructor
Cat.prototype.getColor = function() {
return this.color;
};
const cat = new Cat('小白', 1, '黑色');
console.log(cat.getName()); // 小白
console.log(cat.getColor()); // 黑色
console.log(cat instanceof Animal); // true
完美解决了所有问题:
- 父类构造函数只执行一次
- 原型链完全正确
- 没有任何污染
- 性能最高
- 代码最简洁
六、JS 是动态语言,实例属性会遮蔽原型属性
JavaScript
function Cat() {}
Cat.prototype.species = '猫科动物';
const cat = new Cat();
cat.species = 'hello'; // 在实例上新增属性
console.log(cat.species); // 'hello' → 实例属性
console.log(Cat.prototype.species); // '猫科动物' → 原型属性还在
这正是为什么我们要把方法放在原型上(节省内存),把个性化数据放在实例上。
七、ES6 class 底层到底干了什么?
JavaScript
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() { return this.name; }
}
class Cat extends Animal {
constructor(name, age, color) {
super(name, age);
this.color = color;
}
getColor() { return this.color; }
}
你以为用 class 就摆脱原型了?Babel 转成 ES5 后,核心代码就是:
JavaScript
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
所以 class extends 本质就是寄生组合式继承的语法糖!
八、完整终极工具函数(可直接拷贝到项目中使用)
JavaScript
// 一行搞定最完美的继承
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 可选:方便调用父类方法
Child.super = Parent;
}
// 使用示例
inherit(Cat, Animal);