原型链继承问题相关

对于class继承以及构造函数、原型链有些疑惑,学的不是很好,如果你也想重新学习这块内容请耐心看完

你对 ES6 Class 的理解方向是对的!它确实是构造函数的「语法糖」,但背后封装了更清晰的继承逻辑。我们通过具体例子一步步拆解,从基础到继承,再到 prototype__proto__ 的核心区别,帮你建立完整的认知框架。

一、先搞懂最基础的:Class 本质是「构造函数 + 原型」的语法糖

我们用一个简单的 Person 类为例,对比 ES5 构造函数和 ES6 Class 的写法,你会发现它们本质完全一致:

1. ES6 Class 写法

javascript 复制代码
class Person {
  // 1. 构造函数:new 时自动调用,初始化实例属性
  constructor(name, age) {
    this.name = name; // 实例属性(每个实例独立)
    this.age = age;
  }

  // 2. 原型方法:挂载在 Person.prototype 上(所有实例共享)
  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }

  // 3. 静态方法:挂载在类本身(Person 上),不被实例继承
  static create(name, age) {
    return new Person(name, age); // 类似工厂函数
  }
}

2. 等价的 ES5 构造函数写法

javascript 复制代码
// 1. 构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 2. 原型方法(和 Class 中的 sayHi 完全等价)
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

// 3. 静态方法(和 Class 中的 static create 完全等价)
Person.create = function(name, age) {
  return new Person(name, age);
};

核心结论:

  • class Person { ... } 等价于 function Person() { ... }(构造函数本体)。
  • 类内部的方法(如 sayHi())会被自动挂载到 Person.prototype 上(原型方法,共享复用)。
  • 实例属性(this.name)在构造函数中初始化,每个实例独立拥有。
  • 静态方法(static 修饰)挂载在类本身(Person),只能通过 Person.xxx 调用,不被实例继承。

二、prototype__proto__:最容易混淆的两个属性

这两个属性是理解原型链的核心,我们用「生产机器」和「产品说明书」的比喻来解释:

1. 构造函数.prototype:「实例的说明书」

  • 所有构造函数 (包括 class 定义的类)都有一个 prototype 属性,它是一个对象。
  • 作用:定义「该构造函数创建的所有实例共享的方法和属性」(类似给所有实例发一本通用说明书)。
javascript 复制代码
// Person 是构造函数,Person.prototype 是它的原型对象
console.log(Person.prototype.sayHi); // 能拿到 sayHi 方法(所有实例共享)

2. 实例.__proto__:「指向说明书的指针」

  • 所有对象(包括实例) 都有一个 __proto__ 属性(非标准但浏览器普遍支持,标准中用 Object.getPrototypeOf() 获取)。
  • 作用:指向创建该实例的「构造函数的 prototype」(即实例通过它找到「说明书」)。
javascript 复制代码
const p1 = new Person('Alice', 20);

// p1 的 __proto__ 指向 Person.prototype(说明书的地址)
console.log(p1.__proto__ === Person.prototype); // true

// 实例调用方法时,会通过 __proto__ 找到原型上的方法
p1.sayHi(); // 实际是 p1.__proto__.sayHi.call(p1)

3. 两者的关系:

复制代码
构造函数(Person) → 有一个 prototype 属性 → 指向原型对象(说明书)
   ↑
   |
实例(p1) → 有一个 __proto__ 属性 → 指向同一个原型对象(说明书)

三、原型链:__proto__ 串联起来的「继承链条」

当访问一个对象的属性/方法时,如果自身没有,JS 会通过 __proto__ 向上查找,直到找到或到达链条顶端(null),这就是原型链。

举例:完整的原型链结构

javascript 复制代码
const p1 = new Person('Bob', 25);

// 第一层:p1 自身的属性
console.log(p1.name); // 'Bob'(自身有,不查原型)

// 第二层:p1.__proto__(即 Person.prototype)
console.log(p1.sayHi); // 找到 Person.prototype.sayHi

// 第三层:Person.prototype.__proto__(即 Object.prototype)
console.log(p1.toString()); // 找到 Object.prototype.toString(所有对象的默认方法)

// 第四层:Object.prototype.__proto__ → null(链条终点)
console.log(p1.__proto__.__proto__.__proto__); // null

链条可视化:

复制代码
p1 → p1.__proto__ → Person.prototype → Person.prototype.__proto__ → Object.prototype → Object.prototype.__proto__ → null

四、ES6 Class 继承:extends 背后的原型链逻辑

ES6 的 extends 本质是「寄生组合式继承」的语法糖,我们通过「学生继承自人」的例子拆解:

1. 继承代码示例

javascript 复制代码
// 父类
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() { console.log(`Hi, ${this.name}`); }
}

// 子类继承父类
class Student extends Person {
  constructor(name, grade) {
    super(name); // 必须先调用父类构造函数,初始化 this
    this.grade = grade; // 子类自己的属性
  }
  study() { console.log(`${this.name} is studying in grade ${this.grade}`); }
}

const s1 = new Student('Charlie', 5);

2. 继承后的原型链结构

复制代码
s1 → s1.__proto__ → Student.prototype → Student.prototype.__proto__ → Person.prototype → Person.prototype.__proto__ → Object.prototype → null

3. extends 做了三件关键事:

  • 1. 子类构造函数通过 super 调用父类构造函数

    相当于 ES5 的 Person.call(this, name),确保父类的实例属性被正确初始化。

  • 2. 子类原型的 __proto__ 指向父类原型

    Student.prototype.__proto__ = Person.prototype,保证子类实例能通过原型链访问父类的原型方法(如 sayHi)。

  • 3. 子类的 __proto__ 指向父类

    Student.__proto__ = Person,保证子类能继承父类的静态方法(如果父类有静态方法的话)。

五、关键概念对比表

概念 含义 存在于哪里? 作用
prototype 「原型对象」,存储实例共享的方法/属性 构造函数(类)上 定义所有实例共享的行为
__proto__ 「对象原型指针」,指向创建该对象的构造函数的 prototype 所有对象(包括实例)上 构成原型链,实现属性/方法的继承查找
constructor 指向创建该对象的构造函数(每个原型对象都有) 原型对象上 标识对象的「创造者」,如 p1.constructor === Person

总结:一句话理清核心逻辑

  • prototype 是构造函数的「共享方法仓库」__proto__ 是实例找到这个仓库的「导航指针」。
  • 原型链就是通过 __proto__ 串联起来的仓库查找链,子类继承就是在这个链条上插入新的仓库。
  • ES6 Class 用 classextendssuper 把这些逻辑封装得更直观,但底层和 ES5 构造函数+原型链完全一致。
  1. 子类原型的 proto 指向父类原型:
    即 Student.prototype.proto = Person.prototype,保证子类实例能通过原型链访问父类的原型方法(如 sayHi)。
  2. 子类的 proto 指向父类:
    即 Student.proto = Person,保证子类能继承父类的静态方法(如果父类有静态方法的话)。
    重点疑惑的地方就是3.

你的疑问很关键!这两步确实是 ES6 extends 继承机制中独立且必须的两个环节 ,即使没有静态方法,JavaScript 引擎也会自动执行第 3 步(子类的 __proto__ 指向父类)。这背后的核心原因是:类本身也是一个函数 ,而函数之间的继承关系需要通过 __proto__ 来维护完整的原型链。

先看一个「没有静态方法」的例子,验证实际行为

我们创建一个完全没有静态方法的父类和子类,观察它们的 __proto__ 关系:

javascript 复制代码
// 父类:没有任何静态方法
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() { console.log(`Hi, ${this.name}`); }
}

// 子类:继承自 Person,也没有静态方法
class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  study() { console.log(`${this.name} is studying`); }
}

// 打印验证
console.log(Student.prototype.__proto__ === Person.prototype); // true → 第 2 步生效
console.log(Student.__proto__ === Person); // true → 第 3 步依然生效(即使没有静态方法)

结论 :即使没有静态方法,Student.__proto__ = Person 这一步仍然会被 JavaScript 引擎自动执行。

为什么第 3 步是必须的?(即使没有静态方法)

这要从「类的本质是函数」说起:

  • class Person { ... } 本质上是 function Person() { ... }(构造函数)。
  • 函数本身也是对象,也有自己的 __proto__ 原型链(称为「函数原型链」)。

第 3 步(子类.__proto__ = 父类)的作用是维护「类(函数)自身的原型链」,确保:

1. 类作为「函数对象」的继承完整性

函数本身也可能有属性/方法(比如 Function.prototype 上的 callapply)。Student.__proto__ = Person 保证:

  • 当访问 Student 自身的属性/方法时,如果 Student 没有,会自动去 Person 上查找(再往上是 Function.prototype)。
  • 即使没有静态方法,这个链条也需要存在,因为类本身是函数,函数的原型链需要保持连贯。

2. 确保 instanceof 等操作符的正确性

instanceof 操作符的原理是:判断实例的原型链中是否包含某个构造函数的 prototype

而类之间的继承关系(如 Student 是否继承自 Person)也依赖于类自身的 __proto__ 链条:

javascript 复制代码
// 检查 Student 是否「继承自」Person(本质是检查函数原型链)
console.log(Student instanceof Person); // false(因为 Person 不是函数对象的构造函数)
console.log(Student instanceof Function); // true(因为最终继承自 Function)

如果没有 Student.__proto__ = Person,类之间的继承关系在函数层面会断裂,可能导致一些依赖原型链的操作异常。

3. 为静态方法继承预留「通道」

ES6 设计时考虑了扩展性:即使当前没有静态方法,未来添加时,继承机制能直接生效。

比如后续给父类加静态方法,子类无需修改就能继承:

javascript 复制代码
// 给父类添加静态方法(后加的)
Person.create = function(name) {
  return new Person(name);
};

// 子类自动继承(因为 Student.__proto__ = Person)
const p = Student.create('David'); // 正常工作!
p.sayHi(); // "Hi, David"

一句话总结:两个步骤的本质区别

  • 第 2 步(Student.prototype.__proto__ = Person.prototype):

    维护「实例的原型链」,确保子类实例能继承父类的实例方法 (如 sayHi)。

  • 第 3 步(Student.__proto__ = Person):

    维护「类(函数)自身的原型链」,确保子类能继承父类的静态方法,同时保证类作为函数对象的原型链完整性。

即使没有静态方法,第 3 步也是 JavaScript 引擎为了保持「类继承机制完整性」而自动执行的操作。这正是 ES6 extends 语法糖的优势:它帮我们封装了这些底层细节,不需要像 ES5 那样手动拼接原型链了 😊。