JavaScript 继承终极解析:原型链闭环、六大继承方式对比与所有致命坑
大家好,今天带大家彻底搞懂 JavaScript 中最绕脑、面试最爱问、却又最容易踩坑的知识点——继承。
我们先来了解一下继承的大概知识点:6 种继承方式对比 + 手写 instanceof 然后直接闪现来到本文的核心重点————继承中最烧脑的几个知识点
一、先搞清楚 JavaScript 的“血缘关系”—— 原型链
在开始继承之前,先问你三个灵魂问题:
[].__proto__ // 指向谁?
Array.prototype.__proto__ // 指向谁?
Object.prototype.__proto__ // 是 null!
这就是传说中的原型链:
实例 → 构造函数.prototype → Object.prototype → null
记住这张图,它是后面所有继承的根基:
cat1 → Cat.prototype → Animal.prototype → Object.prototype → null
二、手写 instanceof —— 面试必考
function isInstanceOf(left,right){
let proto = left.__proto__;
while(proto){
if(proto===right.prototype){
return true
}
proto=proto.__proto__;//null
}
return false
}
经典测试:
function Animal() {}
function Cat() {}
Cat.prototype = new Animal();
const cat = new Cat();
console.log(myInstanceof(cat, Animal)); // true
console.log(myInstanceof(cat, Object)); // true
console.log(myInstanceof(cat, Cat)); // true
易错点提醒:
instanceof看的是原型链,不是 constructor!cat.constructor === Animal可能是 false!因为 constructor 很容易被改掉
console.log(cat.constructor === Animal) // 很可能 false!
console.log(cat.constructor === Cat) // 我们后面会修复它
三、JavaScript 继承的六种姿势(从原始到完美)
方式一:构造函数继承(借用构造函数)
function Animal() {
this.species = "动物";
}
function Cat(name, color) {
Animal.call(this); // 借用父类构造函数
this.name = name;
this.color = color;
}
优点:能继承实例属性
缺点:方法全在构造函数里,每次 new 都重新创建,浪费内存 + 拿不到父类原型上的方法
方式二:原型链继承(经典但有坑)
function Animal() {}
Animal.prototype.species = "动物";
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype = new Animal(); // 关键!
Cat.prototype.constructor = Cat; // 手动修复 constructor
优点:能继承原型上的方法
缺点:所有实例共享父类实例的引用属性(大坑!)
Cat.prototype.habits = ['sleep'];
const cat1 = new Cat();
const cat2 = new Cat();
cat1.habits.push('eat');
console.log(cat2.habits); // ["sleep", "eat"] 被污染了!
方式三:直接继承 prototype(副作用爆炸)
Cat.prototype = Animal.prototype; // 千万别写!
后果:给 Cat 加方法,Animal 也被污染了!
Cat.prototype.say = () => console.log('miao');
console.log(Animal.prototype.say); // 也有了!血案!
方式四:圣杯模式(经典中间函数)
function inherit(Child, Parent) {
const F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
// 可选:保留对父类的引用
Child.prototype.uber = Parent.prototype;
}
inherit(Cat, Animal);
这是尤雨溪(Vue 作者)当年在论坛里学的经典写法,至今仍是最佳实践之一。
方式五:ES6 class extends(现代写法,底层还是原型)
class Animal {
constructor() {
this.species = "动物";
}
eat() {
console.log("eating...");
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 必须先调用 super
this.name = name;
this.color = color;
}
}
注意:子类 constructor 里必须先调用 super(),否则 this 会报错!
方式六:终极方案 —— 组合继承(生产推荐)
综合前面两种优点,避免所有缺点:
function Animal() {
this.species = "动物";
}
Animal.prototype.eat = function() {
console.log("eating...");
};
function Cat(name, color) {
Animal.call(this); // 继承实例属性
this.name = name;
this.color = color;
}
Cat.prototype = Object.create(Animal.prototype); // 继承原型
Cat.prototype.constructor = Cat;
Cat.prototype.say = function() {
console.log("miao~");
};
这就是 Vue/React 源码里真正使用的继承方式!
四、终极对比表(建议收藏)
| 方式 | 实例属性 | 原型方法 | 引用属性安全 | constructor正确 | 推荐指数 |
|---|---|---|---|---|---|
| call 继承 | Yes | No | Yes | Yes | 2星 |
| prototype = new Parent() | Yes | Yes | No | Need repair | 3星 |
| 直接继承 prototype | Yes | Yes | Yes | Need repair | 1星(副作用太大) |
| 圣杯模式 | Yes | Yes | Yes | Yes | 4星 |
| Object.create | Yes | Yes | Yes | Yes | 5星 |
| class extends | Yes | Yes | Yes | Yes | 5星(现代首选) |
五、继承中最烧脑的几个知识点
1.直接继承 prototype(最严重、最容易被忽视的副作用)
function Animal() {}
Animal.prototype.species = '动物';
function Cat(name, color) {
this.name = name;
this.color = color;
}
// 危险写法!!!直接把父类的 prototype 赋值给子类
Cat.prototype = Animal.prototype; // ← 这里就是罪魁祸首
副作用一:子类给原型加方法,父类也被污染(双向污染)
// 给 Cat 的原型加一个方法
Cat.prototype.say = function() {
console.log('miao~');
};
console.log(Animal.prototype.say);
// 输出:function say() { console.log('miao~') }
// 什么???Animal 实例怎么也会叫了?!
详细过程图解:
原来的结构:
Cat.prototype → 指向一个对象 A
Animal.prototype → 指向另一个对象 B
执行 Cat.prototype = Animal.prototype 之后:
Cat.prototype → 指向对象 B ←┐
Animal.prototype → 指向对象 B → 同一个对象!
↑
两个构造函数共用同一个原型对象
此时你对 Cat.prototype 做的任何修改,等同于直接修改 Animal.prototype!
const cat = new Cat('小黑', '黑色');
const tiger = new Animal(); // 老虎是 Animal 的实例
cat.say(); // "miao~"
tiger.say(); // "miao~" !!!老虎也会猫叫了,彻底乱套
副作用二:constructor 被彻底搞乱
console.log(Cat.prototype.constructor); // 原来应该是 Cat
// 结果:function Animal() {} ← 被覆盖成了 Animal!!
// 所有 Cat 实例的 constructor 也错了
console.log(cat.constructor === Animal); // true
console.log(cat.constructor === Cat); // false
副作用三:instanceof 完全失灵
console.log(cat instanceof Cat); // true (侥幸还对)
console.log(cat instanceof Animal); // true
// 但是如果你再定义一个 Dog 也这么干:
Dog.prototype = Animal.prototype;
const dog = new Dog();
console.log(dog instanceof Cat); // true!!!狗居然是猫的实例???
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
彻底乱套了!所有继承 Animal 的子类之间互相都是 instanceof true。
结论:直接把 Child.prototype = Parent.prototype 是 JavaScript 继承中的“核弹级”错误,绝对禁止!
2.原型链继承(Cat.prototype = new Animal())的经典副作用:引用类型属性被所有实例共享
function Animal() {
this.habits = ['睡觉', '吃饭']; // 注意:引用类型!
}
Animal.prototype.species = '动物';
function Cat(name, color) {
this.name = name;
this.color = color;
}
// 经典原型链继承(很多人还在用)
Cat.prototype = new Animal(); // ← 这里创建了父类实例
Cat.prototype.constructor = Cat;
const cat1 = new Cat('小黑', '黑色');
const cat2 = new Cat('小白', '白色');
副作用:所有猫共享同一份习惯列表!
cat1.habits.push('玩毛球');
console.log(cat1.habits); // ["睡觉", "吃饭", "玩毛球"]
console.log(cat2.habits); // ["睡觉", "吃饭", "玩毛球"] ← 被污染了!
详细过程图解:
Cat.prototype ← new Animal() 创建的实例
│
├── species: "动物" (原型上的)
└── habits: ["睡觉", "吃饭"] ← 堆内存中的同一个数组!
cat1.__proto__ → Cat.prototype → 这个实例 → habits 数组
cat2.__proto__ → Cat.prototype → 同一个实例 → 同一个 habits 数组
所有 Cat 实例的 habits 指向的是 Cat.prototype(即那个 Animal 实例)上的同一个数组引用!
更隐蔽的坑:即使你不直接访问 habits,也会被污染
function Animal() {
this.friends = [];
}
Cat.prototype = new Animal();
const cat1 = new Cat('小黑');
const cat2 = new Cat('小白');
cat1.friends.push(cat2); // 小黑把小白加为好友
console.log(cat2.friends); // [cat2] 小白居然是自己的好友?!循环引用!
为什么 constructor 需要手动修复?
因为 new Animal() 创建的实例的 constructor 是 Animal:
const temp = new Animal();
console.log(temp.constructor === Animal); // true
Cat.prototype = temp;
// 所以 Cat.prototype.constructor 现在也指向 Animal 了!
console.log(Cat.prototype.constructor === Animal); // true ← 错误!
不手动修复的话:
console.log(cat1.constructor === Animal); // true!明明是 Cat 的实例
这就是为什么经典写法一定要加这一行:
Cat.prototype.constructor = Cat; // 必须手动修复!
正确做法对比(推荐的三种)
| 写法 | 是否有引用共享问题 | 是否污染父类 | constructor 是否正确 | 推荐度 |
|---|---|---|---|---|
Cat.prototype = Animal.prototype | 无 | 严重污染 | 错误 | 0星 |
Cat.prototype = new Animal() | 有(父类实例属性) | 无 | 需要手动修复 | 3星 |
Cat.prototype = Object.create(Animal.prototype) | 无 | 无 | 需要修复或用 defineProperty | 5星 |
class Cat extends Animal | 无 | 无 | 自动正确 | 5星 |
终极推荐写法(杜绝所有副作用)
function Animal(name) {
this.name = name;
this.friends = []; // 即使父类有引用类型也没事
}
Animal.prototype.species = '动物';
function Cat(name, color) {
Animal.call(this, name); // 继承实例属性(各自独立)
this.color = color;
}
// 关键:用 Object.create 创建一个干净的中间对象
Cat.prototype = Object.create(Animal.prototype);
// 完美修复 constructor(推荐这种不可枚举的方式)
Object.defineProperty(Cat.prototype, 'constructor', {
value: Cat,
enumerable: false,
writable: true
});
这样:
- 引用类型属性各自独立
- 不污染父类原型
- constructor 正确
- instanceof 完全正常
这就是 Vue 2.x 中 Vue.extend、React 老版本 createClass 底层真正使用的继承方式!
记住:
直接赋值 prototype = 父类原型 → 核弹
new 父类() 当原型 → 地雷
Object.create + call → 完美
3.你不知道的prototype和__proto__
prototype是构造函数的属性,__proto__是实例的属性
- 什么时候用 prototype(构造函数层面) 使用场景:当你想要定义或修改某个构造函数创建的"所有实例"的共享属性和方法时
text
function Person(name) {
this.name = name;
}
// 使用 prototype - 为所有Person实例添加共享方法
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
Person.prototype.species = 'Human'; // 共享属性
const person1 = new Person('Alice');
const person2 = new Person('Bob');
person1.sayHello(); // 所有实例都能访问prototype上的方法
person2.sayHello();
关键点:
- 只有函数(构造函数)才有 prototype属性
- 用于设置"蓝图"或"模板"
- 修改会影响所有已创建和未来创建的实例
- 什么时候用 proto(实例层面) 使用场景:当你想要查看或操作某个具体实例的原型链时
text
function Person(name) {
this.name = name;
}
const person = new Person('Alice');
// 使用 __proto__ - 查看或修改这个具体实例的原型
console.log(person.__proto__ === Person.prototype); // true
// 检查原型链
console.log(person.__proto__.__proto__ === Object.prototype); // true
// 实际开发中更推荐使用以下方法:
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
console.log(person instanceof Person); // true
关键点:
- 所有对象(包括实例)都有 __proto__属性
- 主要用于调试、检查继承关系
基于第3点引出了最烧脑的一个知识点
“Function 是由 Function 自己 new 出来的, Object 是由 Function new 出来的, Function.prototype 也是由 Object new 出来的…… 这就是一个完美的鸡生蛋蛋生鸡的闭环!”
看完这段话你肯定满脸问号?这是个啥?
别急,我们只需要理解这三句话就能读懂:
-
所有函数(包括 Function 和 Object 自己)都是 Function 的实例 → 所以它们的 proto 统统指向 Function.prototype
-
Object 也是函数,所以 Object.proto === Function.prototype → 这一步把 Object 拉进来了
-
Function.prototype 自己也是一个对象,所以它的 proto 必须指向所有对象的终点 —— Object.prototype → 这一步把 Function.prototype 接回去,闭环完成!
console.log(Function.__proto__ === Function.prototype); // true 自己是自己的爹
console.log(Function.prototype.__proto__ === Object.prototype); // true 爹的爹是 Object.prototype
console.log(Object.__proto__ === Function.prototype); // true Object 的爹是 Function.prototype
console.log(Object.prototype.__proto__ === null); // true 终点
写在最后
JavaScript 的继承,本质就是原型链的游戏。
掌握了原型链,你就掌握了 JavaScript 的灵魂。
记住一句话:
在 JavaScript 中,没有类,只有原型。
class 只是语法糖,extends 只是 Object.create 的甜甜圈。