JavaScript学习笔记:12.类
上一篇吃透了对象这个"万能容器",但创建多个同类型对象时,重复写属性和方法难免繁琐;想隐藏内部逻辑时,没有真正的私有属性也很头疼。这时候,JS的"类(Class)"就该登场了------它是对象的"抽象设计图纸",能批量生产结构统一、封装完善的对象,还能通过继承实现逻辑复用,让代码从"零散对象"升级到"系统化设计"。
新手常被类和原型的关系搞晕,觉得类是"新东西",其实它只是JS原型继承的"语法糖"------本质还是原型那套逻辑,但写法更简洁、更贴近传统面向对象编程思维。今天就用"工厂造车"的比喻,把类的声明、构造、封装、继承讲透,让你既能"画好图纸",又能"造好车"。
一、先搞懂:类是什么?------对象的"设计图纸"
如果把对象比作"一辆辆汽车",那类就是"汽车设计图纸":
- 图纸(类):定义了汽车的统一结构(属性:品牌、型号;方法:行驶、刹车);
- 汽车(对象):根据图纸生产的具体产品,拥有图纸定义的所有属性和方法;
- 工厂(new关键字):根据图纸(类)批量生产汽车(对象)。
之前用构造函数也能批量创建对象,但类的写法更清晰、封装性更好:
js
// 旧方案:构造函数(像手写的简易图纸)
function Car(brand, model) {
this.brand = brand;
this.model = model;
this.drive = function() {
console.log(`开着${this.brand}${this.model}上路`);
};
}
// 新方案:类(规范的正式图纸)
class Car {
// 构造函数:相当于"造车时的初始化流程"
constructor(brand, model) {
this.brand = brand;
this.model = model;
}
// 实例方法:相当于"汽车的功能"
drive() {
console.log(`开着${this.brand}${this.model}上路`);
}
}
// 用图纸造车(实例化)
const myCar = new Car("特斯拉", "Model 3");
myCar.drive(); // 开着特斯拉Model 3上路
核心区别:类的方法会自动挂载到原型上,所有实例共享,避免构造函数中重复创建函数(浪费内存),这也是类的核心优势之一。
二、类的基础操作:从"画图纸"到"造车"
1. 声明类:两种方式,首选声明式
类的声明和函数类似,有"声明式"和"表达式"两种,声明式更直观,是开发首选。
(1)类声明(推荐)
js
// 类声明:首字母大写(约定俗成,区分普通函数)
class Phone {
constructor(brand, price) {
this.brand = brand;
this.price = price;
}
call() {
console.log(`${this.brand}手机打电话`);
}
}
(2)类表达式(匿名/命名)
js
// 匿名类表达式
const Tablet = class {
constructor(brand) {
this.brand = brand;
}
playVideo() {
console.log(`${this.brand}平板看视频`);
}
};
// 命名类表达式(名称仅在类内部可用)
const Laptop = class MyLaptop {
constructor(brand) {
this.brand = brand;
}
code() {
console.log(`${MyLaptop.name}:${this.brand}笔记本写代码`);
}
};
避坑点1:类没有提升,必须先声明再使用
和函数声明不同,类声明不会提升,提前使用会报错(类似let/const):
js
// 反面例子:类未声明就使用
const phone = new Phone(); // ReferenceError: Cannot access 'Phone' before initialization
// 正面例子:先声明,再使用
class Phone {}
const phone = new Phone();
避坑点2:必须用new调用,否则报错
普通构造函数可以不用new调用(this会指向全局),但类必须用new,否则直接报错:
js
// 反面例子:类不用new调用
const car = Car("比亚迪", "汉"); // TypeError: Class constructor Car cannot be invoked without 'new'
// 正面例子:用new实例化
const car = new Car("比亚迪", "汉");
2. 构造函数(constructor):对象的"初始化流程"
constructor是类的特殊方法,相当于"造车时的装配流程",实例化时自动执行,用来初始化对象的属性。
核心规则:
- 一个类只能有一个
constructor,多写会报错; - 实例化时传入的参数,会直接传给
constructor; this指向新创建的实例,给this添加属性就是给实例加属性;- 若没有显式定义
constructor,类会默认生成一个空的构造函数。
js
class User {
constructor(name, age) {
// this指向新创建的User实例
this.name = name;
this.age = age;
this.isAdult = age >= 18; // 初始化时计算衍生属性
}
}
// 实例化时传入参数,自动执行constructor
const user = new User("张三", 25);
console.log(user); // User { name: "张三", age: 25, isAdult: true }
坑点:构造函数的返回值
默认情况下,构造函数会自动返回this(新实例),若手动返回非原始类型(对象/数组),会覆盖this:
js
class Car {
constructor(brand) {
this.brand = brand;
// 反面例子:返回对象,覆盖this
return { model: "Model Y" };
}
}
const car = new Car("特斯拉");
console.log(car.brand); // undefined(this被覆盖)
console.log(car.model); // "Model Y"(返回的对象)
避坑:除非特殊需求,不要在构造函数中手动返回值。
三、类的核心特性:封装、复用与扩展
1. 实例方法:对象的"专属功能"
类中定义的普通方法(不含static关键字)是"实例方法",相当于"汽车的功能",只能通过实例调用,所有实例共享同一个方法(挂载在原型上)。
js
class Dog {
constructor(name) {
this.name = name;
}
// 实例方法:吠叫
bark() {
console.log(`${this.name}汪汪叫`);
}
// 实例方法:摇尾巴
wagTail() {
console.log(`${this.name}摇尾巴`);
}
}
const dog1 = new Dog("旺财");
const dog2 = new Dog("来福");
dog1.bark(); // 旺财汪汪叫
dog2.bark(); // 来福汪汪叫
// 两个实例共享bark方法(原型上的方法)
console.log(dog1.bark === dog2.bark); // true
优势:比构造函数更高效
构造函数中定义的方法,每个实例都会创建一个新函数(浪费内存),而类的实例方法挂载在原型上,所有实例共享,效率更高。
2. 私有字段:对象的"保密配方"
之前用对象时,没有真正的私有属性,外部能随意修改内部数据,就像"汽车的核心零件被人随意改动"。类的私有字段(ES2022新增)完美解决这个问题------用#前缀标记,外部无法访问和修改,只能通过类内部方法操作。
js
class BankAccount {
// 私有字段:用#前缀,必须在类内声明
#balance = 0; // 初始余额(私有,外部不可见)
constructor(name) {
this.name = name; // 公共字段:外部可访问
}
// 公共方法:操作私有字段
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
console.log(`存入${amount}元,余额:${this.#balance}`);
} else {
console.log("存款金额必须大于0");
}
}
// 公共方法:读取私有字段
getBalance() {
return `${this.name}的余额:${this.#balance}`;
}
}
const account = new BankAccount("张三");
account.deposit(1000); // 存入1000元,余额:1000
console.log(account.getBalance()); // 张三的余额:1000
// 外部访问私有字段:报错!
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
account.#balance = 5000; // 报错!无法直接修改
私有字段的核心规则:
- 前缀必须是
#,且是字段名的一部分(不能省略); - 必须在类内部声明,不能在外部动态添加;
- 外部访问/修改会报语法错误(硬私有,无法绕过);
- 私有方法:方法名前加
#,只能在类内部调用。
js
class Calculator {
// 私有方法:内部辅助计算
#add(a, b) {
return a + b;
}
calculate(a, b) {
// 类内部调用私有方法
return this.#add(a, b);
}
}
const calc = new Calculator();
console.log(calc.calculate(2, 3)); // 5
console.log(calc.#add(2, 3)); // SyntaxError: Private method '#add' must be declared in an enclosing class
3. 访问器字段(get/set):属性的"智能门禁"
访问器字段用get(读取)和set(修改)定义,相当于给属性加了"智能门禁",可以在读取/修改时做额外逻辑(验证、转换),用法和普通属性一样。
js
class Temperature {
#celsius = 0; // 私有字段:存储摄氏度
// get访问器:读取华氏度(自动转换)
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
// set访问器:设置华氏度(自动转摄氏度存储)
set fahrenheit(value) {
if (value >= -459.67) { // 绝对零度验证
this.#celsius = (value - 32) * 5/9;
} else {
console.log("温度不能低于绝对零度");
}
}
}
const temp = new Temperature();
// 像访问普通属性一样使用get/set
temp.fahrenheit = 68; // 调用set,转成20℃存储
console.log(temp.fahrenheit); // 68(调用get,从20℃转成68℉)
temp.fahrenheit = -500; // 温度不能低于绝对零度
优势:隐藏转换逻辑,外部用法简洁
如果没有访问器,外部需要手动转换单位,而访问器让外部像操作普通属性一样,内部逻辑被隐藏,更符合"封装"思想。
4. 静态属性(static):类的"公共工具"
静态属性用static关键字定义,属于"类本身",不是实例的属性,相当于"汽车工厂的公共工具",所有实例都能共用,但不能通过实例访问。
js
class MathUtil {
// 静态字段:公共常量
static PI = 3.1415926;
// 静态方法:公共工具函数
static circleArea(radius) {
return this.PI * radius * radius; // 静态方法中this指向类本身
}
}
// 直接通过类访问静态属性/方法
console.log(MathUtil.PI); // 3.1415926
console.log(MathUtil.circleArea(5)); // 78.539815
// 实例无法访问静态属性
const util = new MathUtil();
console.log(util.PI); // undefined
常见场景:
- 存储类的公共常量(如PI);
- 定义工具方法(如数组工具类的排序、过滤方法);
- 工厂方法(创建类实例的快捷方式)。
js
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 静态工厂方法:快速创建成年用户
static createAdultUser(name) {
return new User(name, 18);
}
}
// 用静态方法创建实例,更简洁
const adultUser = User.createAdultUser("李四");
console.log(adultUser.age); // 18
5. 继承(extends):类的"子承父业"
继承用extends关键字实现,相当于"儿子继承父亲的汽车工厂,同时添加自己的生产线"------子类(派生类)能继承父类的属性和方法,还能覆盖或扩展父类逻辑。
基础继承:
js
// 父类:汽车
class Car {
constructor(brand) {
this.brand = brand;
}
drive() {
console.log(`${this.brand}汽车行驶`);
}
}
// 子类:电动车(继承Car)
class ElectricCar extends Car {
constructor(brand, battery) {
// 必须先调用super(),初始化父类构造函数
super(brand); // 相当于new Car(brand)
this.battery = battery; // 子类新增属性:电池容量
}
// 覆盖父类方法
drive() {
console.log(`${this.brand}电动车行驶,剩余电量:${this.battery}kWh`);
}
// 子类新增方法
charge() {
console.log(`${this.brand}电动车充电中`);
}
}
// 实例化子类
const tesla = new ElectricCar("特斯拉", 75);
tesla.drive(); // 特斯拉电动车行驶,剩余电量:75kWh(覆盖父类方法)
tesla.charge(); // 特斯拉电动车充电中(子类新增方法)
// 继承父类的属性
console.log(tesla.brand); // 特斯拉
继承的核心规则:
- 子类构造函数必须先调用
super(),才能使用this(否则报错); super()调用父类的构造函数,传递参数给父类;- 子类可以覆盖父类的方法(重写);
- 子类实例同时是子类和父类的实例(用
instanceof验证)。
js
console.log(tesla instanceof ElectricCar); // true
console.log(tesla instanceof Car); // true
坑点:子类无法访问父类的私有字段
父类的私有字段(#前缀)只能在父类内部访问,子类也无法直接访问,只能通过父类的公共方法间接操作:
js
class Parent {
#privateField = "父类私有字段";
getPrivateField() {
return this.#privateField; // 父类公共方法暴露私有字段
}
}
class Child extends Parent {
getParentPrivate() {
return this.#privateField; // 报错!子类无法访问父类私有字段
}
}
const child = new Child();
console.log(child.getPrivateField()); // 父类私有字段(通过父类公共方法访问)
四、类的实战场景:完整示例
下面用一个"用户管理系统"的示例,整合类的所有核心特性:
js
class User {
// 静态常量:用户角色
static ROLES = {
ADMIN: "管理员",
USER: "普通用户"
};
// 私有字段:密码(保密)
#password;
constructor(username, password, role = User.ROLES.USER) {
this.username = username;
this.#password = password;
this.role = role;
this.createTime = new Date();
}
// 公共方法:验证密码
verifyPassword(password) {
return this.#password === password;
}
// 访问器:读取创建时间(格式化)
get formattedCreateTime() {
return this.createTime.toLocaleString();
}
// 静态方法:批量创建用户
static createUsers(userList) {
return userList.map(item => new User(item.username, item.password, item.role));
}
}
// 子类:管理员用户(继承User)
class AdminUser extends User {
constructor(username, password) {
super(username, password, User.ROLES.ADMIN);
}
// 管理员专属方法:删除用户
deleteUser(userId) {
console.log(`管理员${this.username}删除用户${userId}`);
}
}
// 1. 创建普通用户
const user = new User("zhangsan", "123456");
console.log(user.verifyPassword("123456")); // true
console.log(user.formattedCreateTime); // 格式化的创建时间
// 2. 创建管理员用户
const admin = new AdminUser("admin", "admin123");
admin.deleteUser("1001"); // 管理员admin删除用户1001
// 3. 批量创建用户
const users = User.createUsers([
{ username: "lisi", password: "654321" },
{ username: "wangwu", password: "888888", role: User.ROLES.ADMIN }
]);
console.log(users.length); // 2
五、类的避坑总结
- 类没有提升,必须先声明再实例化;
- 必须用
new调用类,否则报错; - 私有字段用
#前缀,外部无法访问,必须在类内声明; - 子类构造函数必须先调用
super(),才能使用this; - 静态属性属于类本身,实例无法访问;
- 父类私有字段子类无法直接访问,需通过父类公共方法暴露。
六、什么时候用类?------ 类的适用场景
类不是万能的,以下场景优先用类:
- 需要创建多个结构相同、有内部状态的对象(如用户、商品、汽车);
- 需要隐藏内部逻辑(如密码、核心算法),只暴露公共接口;
- 需要继承和多态(如普通用户、管理员用户,有共同行为但有差异);
- 代码需要结构化组织(如工具类、组件类)。
如果只是简单存储数据(如配置项),用普通对象更简洁;如果是纯函数逻辑(如工具函数),用模块导出函数更合适。
七、总结:类的本质是"优雅的原型封装"
JS的类本质是原型继承的语法糖,但它让代码更简洁、封装性更好、继承更清晰,尤其适合大型项目的结构化开发。记住核心比喻:
- 类 = 设计图纸;
- 实例 = 按图纸造的产品;
- 构造函数 = 初始化流程;
- 实例方法 = 产品功能;
- 静态属性 = 工厂工具;
- 继承 = 子工厂继承父工厂。
掌握类的用法,能让你从"零散写对象"升级到"系统化设计代码",这也是前端工程师从入门到进阶的关键一步。