对于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 用
class
、extends
、super
把这些逻辑封装得更直观,但底层和 ES5 构造函数+原型链完全一致。
- 子类原型的 proto 指向父类原型:
即 Student.prototype.proto = Person.prototype,保证子类实例能通过原型链访问父类的原型方法(如 sayHi)。 - 子类的 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
上的 call
、apply
)。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 那样手动拼接原型链了 😊。