JavaScript 继承终极解析:原型链闭环、六大继承方式对比与所有致命坑

360 阅读8分钟

JavaScript 继承终极解析:原型链闭环、六大继承方式对比与所有致命坑

大家好,今天带大家彻底搞懂 JavaScript 中最绕脑、面试最爱问、却又最容易踩坑的知识点——继承

我们先来了解一下继承的大概知识点:6 种继承方式对比 + 手写 instanceof 然后直接闪现来到本文的核心重点————继承中最烧脑的几个知识点

7d51c79f4346e65c2eafb673286d21c7.jpg

一、先搞清楚 JavaScript 的“血缘关系”—— 原型链

在开始继承之前,先问你三个灵魂问题:

[].__proto__               // 指向谁?
Array.prototype.__proto__  // 指向谁?
Object.prototype.__proto__ // 是 null!

这就是传说中的原型链:

实例 → 构造函数.prototypeObject.prototypenull

记住这张图,它是后面所有继承的根基:

cat1 → Cat.prototypeAnimal.prototypeObject.prototypenull

二、手写 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 继承YesNoYesYes2星
prototype = new Parent()YesYesNoNeed repair3星
直接继承 prototypeYesYesYesNeed repair1星(副作用太大)
圣杯模式YesYesYesYes4星
Object.createYesYesYesYes5星
class extendsYesYesYesYes5星(现代首选)

五、继承中最烧脑的几个知识点

7371e6c646ae0a7919845d85490c9946.jpg

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.prototypenew Animal() 创建的实例
                  │
                  ├── species: "动物" (原型上的)
                  └── habits: ["睡觉", "吃饭"] ← 堆内存中的同一个数组!

cat1.__proto__Cat.prototype → 这个实例 → habits 数组
cat2.__proto__Cat.prototype → 同一个实例 → 同一个 habits 数组

image.png

所有 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)需要修复或用 defineProperty5星
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__是实例的属性

  1. 什么时候用 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属性
  • 用于设置"蓝图"或"模板"
  • 修改会影响所有已创建和未来创建的实例
  1. 什么时候用 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 出来的…… 这就是一个完美的鸡生蛋蛋生鸡的闭环!”

看完这段话你肯定满脸问号?这是个啥?

a8a0956310fbc3378caa4831a0918c2b.jpg 别急,我们只需要理解这三句话就能读懂:

  • 所有函数(包括 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 的甜甜圈。