如何声明一个类?类如何继承?------ JavaScript 类与继承完全指南
一、开篇:类 ------JavaScript 面向对象编程的核心载体
在 JavaScript 的发展历程中,从早期的构造函数 + 原型链 实现面向对象,到 ES6 引入class关键字提供更规范、更贴近传统面向对象语言的类语法,类已经成为构建可复用、可维护、层次化代码的核心载体。
无论是 React/Vue 中的组件开发、Node.js 中的模块封装,还是复杂业务逻辑的抽象,都离不开类的声明与继承。很多初学者容易混淆 "类的声明方式""原型方法与实例方法的区别",也会在继承中遇到 "构造函数调用""方法重写""超类访问" 等问题,导致写出的代码层次混乱、难以扩展。
本文将从类的声明(基础到进阶) 、类的继承(核心方法与细节) 、继承的实战场景与避坑指南三个维度,结合完整示例深度拆解 JavaScript 类与继承的精髓,帮你彻底掌握类的声明技巧与继承逻辑,写出符合现代前端开发规范的面向对象代码。
二、第一部分:如何声明一个类?(从基础到进阶)
ES6 提供的class语法是声明类的标准方式(本质是构造函数 + 原型链的语法糖,但更简洁、更具可读性),同时也保留了传统的 "构造函数 + 原型" 声明方式,我们重点讲解现代开发中主流的class声明法,同时补充传统方式作为底层理解参考。
1. 基础声明:使用class关键字(核心方式)
(1)核心语法
使用class关键字声明类,类名遵循首字母大写 的约定俗成(区分普通函数,提升代码可读性),核心包含constructor构造函数和类方法(原型方法)。
javascript
运行
// 基础类声明语法
class ClassName {
// 1. 构造函数:初始化实例属性(可选,若省略会默认生成空构造函数)
constructor(param1, param2) {
// 实例属性:通过this绑定,每个实例拥有独立的属性副本
this.prop1 = param1;
this.prop2 = param2;
}
// 2. 原型方法:类的核心方法,所有实例共享该方法(挂载在原型上)
method1() {
// 方法逻辑中可通过this访问实例属性和其他方法
return `实例属性:${this.prop1},${this.prop2}`;
}
// 3. 多个原型方法:用逗号分隔(ES6语法中可省略逗号,更简洁)
method2() {
console.log("这是类的另一个原型方法");
}
}
(2)实战示例:声明一个User类
javascript
运行
// 声明一个User类
class User {
// 构造函数:初始化用户名和年龄
constructor(name, age) {
this.name = name; // 实例属性:用户名
this.age = age; // 实例属性:年龄
}
// 原型方法:展示用户信息
showInfo() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
}
// 原型方法:修改用户年龄(带逻辑校验)
setAge(newAge) {
if (typeof newAge === "number" && newAge >= 0 && newAge <= 150) {
this.age = newAge;
console.log(`年龄修改成功,当前年龄:${this.age}岁`);
} else {
console.log("输入年龄不合法,请输入0-150之间的数字");
}
}
}
// 实例化类:使用new关键字创建类的实例(必须带new,否则报错)
const user1 = new User("张三", 25);
const user2 = new User("李四", 28);
// 调用实例的原型方法
user1.showInfo(); // 输出:你好,我是张三,今年25岁
user1.setAge(26); // 输出:年龄修改成功,当前年龄:26岁
user2.showInfo(); // 输出:你好,我是李四,今年28岁
// 验证:所有实例共享原型方法(内存复用,提升性能)
console.log(user1.showInfo === user2.showInfo); // 输出:true
(3)核心要点说明
constructor构造函数:
- 用于初始化实例属性,是类的 "入口",当使用
new创建实例时,会自动调用constructor; - 可选省略,若省略,JS 引擎会默认生成一个空的
constructor()(constructor() {}); - 内部通过
this绑定的属性是实例属性,每个实例拥有独立的副本,互不干扰; - 不能返回非对象类型的值(若返回对象,会覆盖
new创建的实例,不推荐)。
- 类方法(原型方法):
- 类内部直接定义的方法(如
showInfo())是原型方法 ,挂载在类的prototype属性上; - 所有实例共享该方法,无需为每个实例创建独立的方法副本,节省内存;
- 方法内部的
this指向调用该方法的实例(若单独提取方法,需注意this绑定问题)。
- 实例化类:
- 必须使用
new关键字调用类,否则会抛出TypeError(类不能像普通函数一样直接调用); - 实例化时传入的参数,会直接传递给
constructor构造函数。
2. 进阶声明:类的其他成员(静态方法、私有成员、get/set 访问器)
除了基础的构造函数和原型方法,现代 JavaScript 类还支持静态方法、私有成员、get/set访问器等高级特性,进一步丰富类的功能。
(1)静态方法:使用static关键字(类本身的方法)
静态方法是挂载在类本身的方法,不能被实例调用,只能通过类名调用,常用于定义工具方法、创建实例的工厂方法。
javascript
运行
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
}
// 静态方法:用static关键字声明
static createAdmin() {
// 静态方法内部可返回类的实例(工厂方法)
return new User("系统管理员", 0);
}
// 静态工具方法:格式化用户信息
static formatUser(user) {
return `[用户信息] 姓名:${user.name},年龄:${user.age}`;
}
}
// 1. 调用静态方法创建实例(不能通过实例调用)
const admin = User.createAdmin();
admin.showInfo(); // 输出:你好,我是系统管理员,今年0岁
// 2. 调用静态工具方法
const formatStr = User.formatUser(admin);
console.log(formatStr); // 输出:[用户信息] 姓名:系统管理员,年龄:0
// 3. 实例调用静态方法:报错(静态方法不属于实例)
console.log(admin.createAdmin); // 输出:undefined
// admin.createAdmin(); // 抛出:TypeError: admin.createAdmin is not a function
(2)私有成员:#前缀(ES2022+,受保护的成员)
私有成员(私有属性、私有方法)仅能在类内部访问,外部和子类都无法直接访问,用于保护类的内部状态,避免外部意外篡改。
javascript
运行
class User {
// 私有属性:用#前缀声明(必须在类内部提前声明)
#password; // 私有属性:用户密码
constructor(name, age, password) {
this.name = name;
this.age = age;
this.#password = password; // 初始化私有属性
}
showInfo() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
}
// 私有方法:用#前缀声明(仅类内部可调用)
#checkPassword(pwd) {
return this.#password === pwd;
}
// 公共方法:间接访问私有成员(对外暴露规范接口)
verifyPassword(inputPwd) {
const isCorrect = this.#checkPassword(inputPwd);
console.log(isCorrect ? "密码验证通过" : "密码验证失败");
return isCorrect;
}
}
const user = new User("张三", 25, "123456");
// 1. 调用公共方法间接访问私有成员
user.verifyPassword("123456"); // 输出:密码验证通过
user.verifyPassword("654321"); // 输出:密码验证失败
// 2. 外部直接访问私有成员:报错(私有成员对外隐藏)
console.log(user.#password); // 抛出:SyntaxError: Private field '#password' must be declared in an enclosing class
// user.#checkPassword("123456"); // 抛出:SyntaxError: Private field '#checkPassword' must be declared in an enclosing class
(3)get/set访问器:优雅操作实例属性
get和set访问器用于定义实例属性的 "读取" 和 "修改" 逻辑,允许我们在访问 / 修改属性时添加额外处理(如校验、格式化),让属性操作更优雅、更安全。
javascript
运行
class User {
constructor(name, age) {
this.name = name;
this._age = age; // 约定:下划线前缀表示"受保护属性"(仅规范,非语法层面私有)
}
// get访问器:读取age属性时触发
get age() {
console.log("正在读取年龄属性");
return this._age;
}
// set访问器:修改age属性时触发
set age(newAge) {
console.log("正在修改年龄属性");
if (typeof newAge === "number" && newAge >= 0 && newAge <= 150) {
this._age = newAge;
} else {
console.log("输入年龄不合法,无法修改");
}
}
}
const user = new User("张三", 25);
// 1. 读取age属性:触发get访问器
console.log(user.age); // 输出:正在读取年龄属性 → 25
// 2. 修改age属性:触发set访问器
user.age = 26; // 输出:正在修改年龄属性
console.log(user.age); // 输出:正在读取年龄属性 → 26
// 3. 传入不合法年龄:触发set访问器的校验逻辑
user.age = -10; // 输出:正在修改年龄属性 → 输入年龄不合法,无法修改
3. 补充:传统声明方式(构造函数 + 原型链)
在 ES6 之前,没有class关键字,开发者通过 "构造函数 + 原型链" 实现类的功能,这是class语法的底层实现,理解该方式有助于深入掌握类的本质。
javascript
运行
// 1. 构造函数:对应class的constructor
function User(name, age) {
this.name = name;
this.age = age;
}
// 2. 原型方法:对应class的原型方法,所有实例共享
User.prototype.showInfo = function() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
};
User.prototype.setAge = function(newAge) {
if (typeof newAge === "number" && newAge >= 0 && newAge <= 150) {
this.age = newAge;
}
};
// 3. 实例化:与class一致,使用new关键字
const user = new User("张三", 25);
user.showInfo(); // 输出:你好,我是张三,今年25岁
// 验证:class本质是构造函数的语法糖
console.log(typeof User); // 输出:function
console.log(User.prototype === user.__proto__); // 输出:true
三、第二部分:类如何继承?(核心方法与细节)
继承是面向对象编程的三大特性之一,用于实现代码复用和层次化抽象------ 子类(派生类)可以继承父类(超类、基类)的属性和方法,同时可以扩展自己的专属功能,或重写父类的方法。
JavaScript 中类的继承核心是使用extends关键字(实现类的继承)和super关键字(访问父类成员),这是现代开发的标准方式。
1. 基础继承:使用extends关键字(实现父类属性与方法的继承)
(1)核心语法
javascript
运行
// 父类(超类、基类)
class ParentClass {
constructor(param1) {
this.parentProp = param1;
}
parentMethod() {
console.log("这是父类的原型方法");
return this.parentProp;
}
}
// 子类(派生类):使用extends关键字继承父类
class ChildClass extends ParentClass {
constructor(param1, param2) {
// 必须先调用super():初始化父类构造函数(否则报错)
super(param1);
// 再初始化子类自己的实例属性
this.childProp = param2;
}
// 子类自己的原型方法(扩展功能)
childMethod() {
console.log("这是子类的原型方法");
return this.childProp;
}
}
(2)实战示例:AdminUser子类继承User父类
javascript
运行
// 父类:User类
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo() {
console.log(`姓名:${this.name},年龄:${this.age}`);
}
setAge(newAge) {
if (typeof newAge === "number" && newAge >= 0 && newAge <= 150) {
this.age = newAge;
console.log(`年龄修改为:${this.age}岁`);
} else {
console.log("年龄输入不合法");
}
}
}
// 子类:AdminUser类(继承User类)
class AdminUser extends User {
constructor(name, age, role) {
// 1. 调用super():传入父类构造函数所需的参数,初始化父类属性
super(name, age);
// 2. 初始化子类专属属性
this.role = role; // 子类专属属性:管理员角色(如"超级管理员"、"普通管理员")
}
// 3. 子类专属方法:扩展父类没有的功能
showRole() {
console.log(`角色:${this.role}`);
}
}
// 实例化子类
const admin = new AdminUser("系统管理员", 0, "超级管理员");
// 1. 调用继承自父类的方法
admin.showInfo(); // 输出:姓名:系统管理员,年龄:0
admin.setAge(1); // 输出:年龄修改为:1岁
// 2. 调用子类自己的专属方法
admin.showRole(); // 输出:角色:超级管理员
// 3. 验证:子类实例同时拥有父类和子类的属性
console.log(admin.name, admin.age, admin.role); // 输出:系统管理员 1 超级管理员
(3)核心要点说明
extends关键字:
- 用于声明子类继承父类,子类会自动继承父类的所有公共实例属性 和原型方法(私有成员无法继承);
- 支持链式继承(如
GrandChild extends Child extends Parent),但不推荐过深的继承链(增加代码复杂度)。
super关键字的核心作用(必须掌握):
- 在子类构造函数中,必须先调用
super(),才能使用this关键字(否则会抛出ReferenceError); super()本质是调用父类的构造函数,需传入父类构造函数所需的参数;- 若子类省略
constructor,JS 引擎会默认生成一个构造函数,自动调用super()并传递所有参数(constructor(...args) { super(...args); })。
2. 进阶继承:方法重写与super访问父类方法
子类不仅可以继承父类的方法,还可以重写(覆盖)父类的方法 (实现子类专属的逻辑),同时可以通过super.方法名()在重写的方法中访问父类的原方法,实现 "扩展父类方法" 而非 "完全覆盖"。
(1)方法重写:子类覆盖父类的方法
javascript
运行
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo() {
console.log(`【父类方法】姓名:${this.name},年龄:${this.age}`);
}
}
class AdminUser extends User {
constructor(name, age, role) {
super(name, age);
this.role = role;
}
// 方法重写:子类覆盖父类的showInfo()方法
showInfo() {
console.log(`【子类方法】姓名:${this.name},年龄:${this.age},角色:${this.role}`);
}
}
const admin = new AdminUser("系统管理员", 0, "超级管理员");
admin.showInfo(); // 输出:【子类方法】姓名:系统管理员,年龄:0,角色:超级管理员
(2)扩展父类方法:通过super访问父类原方法
很多场景下,我们不需要完全覆盖父类方法,而是在父类方法的基础上扩展功能,此时可以通过super.方法名()调用父类的原方法。
javascript
运行
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo() {
console.log(`姓名:${this.name},年龄:${this.age}`);
}
}
class AdminUser extends User {
constructor(name, age, role) {
super(name, age);
this.role = role;
}
// 扩展父类方法:先调用父类原方法,再添加子类专属逻辑
showInfo() {
// 调用父类的showInfo()方法(通过super访问)
super.showInfo();
// 子类专属扩展逻辑
console.log(`角色:${this.role}`);
}
}
const admin = new AdminUser("系统管理员", 0, "超级管理员");
admin.showInfo();
// 输出结果:
// 姓名:系统管理员,年龄:0
// 角色:超级管理员
3. 特殊继承:静态方法的继承与私有成员的继承规则
(1)静态方法的继承
子类会自动继承父类的静态方法,可通过子类名.静态方法名()调用,无需额外配置。
javascript
运行
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 父类静态方法
static formatUser(user) {
return `[用户] 姓名:${user.name},年龄:${user.age}`;
}
}
class AdminUser extends User {
constructor(name, age, role) {
super(name, age);
this.role = role;
}
}
const admin = new AdminUser("系统管理员", 0, "超级管理员");
// 子类继承父类的静态方法,通过子类名调用
const formatStr = AdminUser.formatUser(admin);
console.log(formatStr); // 输出:[用户] 姓名:系统管理员,年龄:0
(2)私有成员的继承规则
父类的私有成员(#前缀声明)无法被子类继承,子类既不能直接访问父类的私有成员,也不能重写父类的私有方法,私有成员仅在定义它的类内部可见。
javascript
运行
class User {
#password; // 父类私有属性
constructor(name, age, password) {
this.name = name;
this.age = age;
this.#password = password;
}
// 父类私有方法
#checkPassword(pwd) {
return this.#password === pwd;
}
// 父类公共方法:间接访问私有成员
verifyPassword(pwd) {
return this.#checkPassword(pwd);
}
}
class AdminUser extends User {
constructor(name, age, password, role) {
super(name, age, password);
this.role = role;
}
// 子类尝试访问父类私有成员:报错
showPassword() {
// console.log(this.#password); // 抛出:SyntaxError: Private field '#password' must be declared in an enclosing class
}
}
const admin = new AdminUser("系统管理员", 0, "123456", "超级管理员");
// 子类可调用父类公共方法,但无法直接访问父类私有成员
console.log(admin.verifyPassword("123456")); // 输出:true
四、第三部分:继承的实战避坑指南
1. 坑 1:子类构造函数中未先调用super(),使用this报错
问题现象 :子类构造函数中,在调用super()之前使用this,会抛出ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor。
解决方案 :子类构造函数中,必须先调用super(),再初始化子类的实例属性(使用this) ,super()是初始化父类实例的关键,只有父类实例初始化完成,子类才能安全地使用this。
javascript
运行
class AdminUser extends User {
constructor(name, age, role) {
// 错误:先使用this,后调用super()
// this.role = role; // 抛出错误
// 正确:先调用super(),再使用this
super(name, age);
this.role = role;
}
}
2. 坑 2:重写父类方法时,忽略super导致父类逻辑丢失
问题现象 :子类重写父类方法时,直接编写子类逻辑,未调用super.方法名(),导致父类方法中的核心逻辑丢失,出现功能异常。
解决方案 :若需要扩展父类方法而非完全覆盖,务必在子类重写的方法中调用super.方法名(),保留父类核心逻辑。
3. 坑 3:过深的继承链导致代码难以维护
问题现象 :使用链式继承(如A extends B extends C extends D),继承链过深,当需要修改某个类的逻辑时,会影响所有子类,且代码追踪困难。
解决方案:
- 控制继承链深度,尽量不超过 3 层;
- 优先使用 "组合" 而非 "继承" 实现功能复用(如将公共逻辑封装为模块,在类中引入使用);
- 遵循 "里氏替换原则":子类必须能够替换父类,且不改变父类的核心行为。
4. 坑 4:混淆实例方法与静态方法的继承规则
问题现象:尝试通过子类实例调用父类的静态方法,导致报错。
解决方案 :明确静态方法的调用规则 ------静态方法属于类本身,只能通过类名调用,不能通过实例调用,子类继承的静态方法也遵循该规则。
五、总结:类与继承的核心知识点回顾与实战准则
- 类的声明核心:
- 现代开发优先使用
class关键字,核心包含constructor构造函数(初始化实例属性)和原型方法(所有实例共享); - 高级特性:
static静态方法(类本身的方法)、#私有成员(类内部保护)、get/set访问器(优雅操作属性)。
- 类的继承核心:
- 基础继承:使用
extends关键字实现子类继承父类,子类自动继承父类的公共实例属性和原型方法; - 关键细节:子类构造函数中必须先调用
super(),才能使用this; - 进阶操作:方法重写(覆盖父类方法)、
super访问父类方法(扩展父类功能)。
- 实战准则:
- 类名首字母大写,方法命名语义化,提升代码可读性;
- 优先使用组合而非继承实现功能复用,控制继承链深度;
- 私有成员用于保护内部状态,对外暴露规范的公共接口;
- 重写父类方法时,按需保留父类逻辑(通过
super),避免功能丢失。
类与继承是 JavaScript 面向对象编程的核心,掌握它们不仅能写出更具层次化、可复用的代码,更能深入理解现代前端框架的底层设计思想(如 React 的组件继承、Vue 的混入机制)。
最后用一句话总结:类是抽象业务逻辑的载体,继承是实现代码复用的桥梁,合理声明类、优雅使用继承,才能构建出健壮、可扩展的前端应用。