找回密码
 立即注册
首页 业界区 业界 原型链继承问题相关

原型链继承问题相关

癖艺泣 6 天前
对于class继承以及构造函数、原型链有些疑惑,学的不是很好,如果你也想重新学习这块内容请耐心看完
你对 ES6 Class 的理解方向是对的!它确实是构造函数的「语法糖」,但背后封装了更清晰的继承逻辑。我们通过具体例子一步步拆解,从基础到继承,再到 prototype 和 __proto__ 的核心区别,帮你建立完整的认知框架。
一、先搞懂最基础的:Class 本质是「构造函数 + 原型」的语法糖

我们用一个简单的 Person 类为例,对比 ES5 构造函数和 ES6 Class 的写法,你会发现它们本质完全一致:
1. ES6 Class 写法
  1. class Person {
  2.   // 1. 构造函数:new 时自动调用,初始化实例属性
  3.   constructor(name, age) {
  4.     this.name = name; // 实例属性(每个实例独立)
  5.     this.age = age;
  6.   }
  7.   // 2. 原型方法:挂载在 Person.prototype 上(所有实例共享)
  8.   sayHi() {
  9.     console.log(`Hi, I'm ${this.name}`);
  10.   }
  11.   // 3. 静态方法:挂载在类本身(Person 上),不被实例继承
  12.   static create(name, age) {
  13.     return new Person(name, age); // 类似工厂函数
  14.   }
  15. }
复制代码
2. 等价的 ES5 构造函数写法
  1. // 1. 构造函数
  2. function Person(name, age) {
  3.   this.name = name;
  4.   this.age = age;
  5. }
  6. // 2. 原型方法(和 Class 中的 sayHi 完全等价)
  7. Person.prototype.sayHi = function() {
  8.   console.log(`Hi, I'm ${this.name}`);
  9. };
  10. // 3. 静态方法(和 Class 中的 static create 完全等价)
  11. Person.create = function(name, age) {
  12.   return new Person(name, age);
  13. };
复制代码
核心结论:


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

这两个属性是理解原型链的核心,我们用「生产机器」和「产品说明书」的比喻来解释:
1. 构造函数.prototype:「实例的说明书」


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


  • 所有对象(包括实例) 都有一个 __proto__ 属性(非标准但浏览器普遍支持,标准中用 Object.getPrototypeOf() 获取)。
  • 作用:指向创建该实例的「构造函数的 prototype」(即实例通过它找到「说明书」)。
  1. const p1 = new Person('Alice', 20);
  2. // p1 的 __proto__ 指向 Person.prototype(说明书的地址)
  3. console.log(p1.__proto__ === Person.prototype); // true
  4. // 实例调用方法时,会通过 __proto__ 找到原型上的方法
  5. p1.sayHi(); // 实际是 p1.__proto__.sayHi.call(p1)
复制代码
3. 两者的关系:
  1. 构造函数(Person) → 有一个 prototype 属性 → 指向原型对象(说明书)
  2.    ↑
  3.    |
  4. 实例(p1) → 有一个 __proto__ 属性 → 指向同一个原型对象(说明书)
复制代码
三、原型链:__proto__ 串联起来的「继承链条」

当访问一个对象的属性/方法时,如果自身没有,JS 会通过 __proto__ 向上查找,直到找到或到达链条顶端(null),这就是原型链。
举例:完整的原型链结构
  1. const p1 = new Person('Bob', 25);
  2. // 第一层:p1 自身的属性
  3. console.log(p1.name); // 'Bob'(自身有,不查原型)
  4. // 第二层:p1.__proto__(即 Person.prototype)
  5. console.log(p1.sayHi); // 找到 Person.prototype.sayHi
  6. // 第三层:Person.prototype.__proto__(即 Object.prototype)
  7. console.log(p1.toString()); // 找到 Object.prototype.toString(所有对象的默认方法)
  8. // 第四层:Object.prototype.__proto__ → null(链条终点)
  9. console.log(p1.__proto__.__proto__.__proto__); // null
复制代码
链条可视化:
  1. p1 → p1.__proto__ → Person.prototype → Person.prototype.__proto__ → Object.prototype → Object.prototype.__proto__ → null
复制代码
四、ES6 Class 继承:extends 背后的原型链逻辑

ES6 的 extends 本质是「寄生组合式继承」的语法糖,我们通过「学生继承自人」的例子拆解:
1. 继承代码示例
  1. // 父类
  2. class Person {
  3.   constructor(name) {
  4.     this.name = name;
  5.   }
  6.   sayHi() { console.log(`Hi, ${this.name}`); }
  7. }
  8. // 子类继承父类
  9. class Student extends Person {
  10.   constructor(name, grade) {
  11.     super(name); // 必须先调用父类构造函数,初始化 this
  12.     this.grade = grade; // 子类自己的属性
  13.   }
  14.   study() { console.log(`${this.name} is studying in grade ${this.grade}`); }
  15. }
  16. const s1 = new Student('Charlie', 5);
复制代码
2. 继承后的原型链结构
  1. 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 用 class、extends、super 把这些逻辑封装得更直观,但底层和 ES5 构造函数+原型链完全一致。

  • 子类原型的 proto 指向父类原型:
    即 Student.prototype.proto = Person.prototype,保证子类实例能通过原型链访问父类的原型方法(如 sayHi)。
  • 子类的 proto 指向父类:
    即 Student.proto = Person,保证子类能继承父类的静态方法(如果父类有静态方法的话)。
    重点疑惑的地方就是3.
你的疑问很关键!这两步确实是 ES6 extends 继承机制中独立且必须的两个环节,即使没有静态方法,JavaScript 引擎也会自动执行第 3 步(子类的 __proto__ 指向父类)。这背后的核心原因是:类本身也是一个函数,而函数之间的继承关系需要通过 __proto__ 来维护完整的原型链。
先看一个「没有静态方法」的例子,验证实际行为

我们创建一个完全没有静态方法的父类和子类,观察它们的 __proto__ 关系:
  1. // 父类:没有任何静态方法
  2. class Person {
  3.   constructor(name) {
  4.     this.name = name;
  5.   }
  6.   sayHi() { console.log(`Hi, ${this.name}`); }
  7. }
  8. // 子类:继承自 Person,也没有静态方法
  9. class Student extends Person {
  10.   constructor(name, grade) {
  11.     super(name);
  12.     this.grade = grade;
  13.   }
  14.   study() { console.log(`${this.name} is studying`); }
  15. }
  16. // 打印验证
  17. console.log(Student.prototype.__proto__ === Person.prototype); // true → 第 2 步生效
  18. console.log(Student.__proto__ === Person); // true → 第 3 步依然生效(即使没有静态方法)
复制代码
结论:即使没有静态方法,Student.__proto__ = Person 这一步仍然会被 JavaScript 引擎自动执行。
为什么第 3 步是必须的?(即使没有静态方法)

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

  • class Person { ... } 本质上是 function Person() { ... }(构造函数)。
  • 函数本身也是对象,也有自己的 __proto__ 原型链(称为「函数原型链」)。
第 3 步(子类.__proto__ = 父类)的作用是维护「类(函数)自身的原型链」,确保:
1. 类作为「函数对象」的继承完整性

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

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

instanceof 操作符的原理是:判断实例的原型链中是否包含某个构造函数的 prototype
而类之间的继承关系(如 Student 是否继承自 Person)也依赖于类自身的 __proto__ 链条:
  1. // 检查 Student 是否「继承自」Person(本质是检查函数原型链)
  2. console.log(Student instanceof Person); // false(因为 Person 不是函数对象的构造函数)
  3. console.log(Student instanceof Function); // true(因为最终继承自 Function)
复制代码
如果没有 Student.__proto__ = Person,类之间的继承关系在函数层面会断裂,可能导致一些依赖原型链的操作异常。
3. 为静态方法继承预留「通道」

ES6 设计时考虑了扩展性:即使当前没有静态方法,未来添加时,继承机制能直接生效。
比如后续给父类加静态方法,子类无需修改就能继承:
  1. // 给父类添加静态方法(后加的)
  2. Person.create = function(name) {
  3.   return new Person(name);
  4. };
  5. // 子类自动继承(因为 Student.__proto__ = Person)
  6. const p = Student.create('David'); // 正常工作!
  7. p.sayHi(); // "Hi, David"
复制代码
一句话总结:两个步骤的本质区别


  • 第 2 步(Student.prototype.__proto__ = Person.prototype):
    维护「实例的原型链」,确保子类实例能继承父类的实例方法(如 sayHi)。
  • 第 3 步(Student.__proto__ = Person):
    维护「类(函数)自身的原型链」,确保子类能继承父类的静态方法,同时保证类作为函数对象的原型链完整性。

即使没有静态方法,第 3 步也是 JavaScript 引擎为了保持「类继承机制完整性」而自动执行的操作。这正是 ES6 extends 语法糖的优势:它帮我们封装了这些底层细节,不需要像 ES5 那样手动拼接原型链了
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册