JavaScript学习笔记:12.类

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

五、类的避坑总结

  1. 类没有提升,必须先声明再实例化;
  2. 必须用new调用类,否则报错;
  3. 私有字段用#前缀,外部无法访问,必须在类内声明;
  4. 子类构造函数必须先调用super(),才能使用this
  5. 静态属性属于类本身,实例无法访问;
  6. 父类私有字段子类无法直接访问,需通过父类公共方法暴露。

六、什么时候用类?------ 类的适用场景

类不是万能的,以下场景优先用类:

  1. 需要创建多个结构相同、有内部状态的对象(如用户、商品、汽车);
  2. 需要隐藏内部逻辑(如密码、核心算法),只暴露公共接口;
  3. 需要继承和多态(如普通用户、管理员用户,有共同行为但有差异);
  4. 代码需要结构化组织(如工具类、组件类)。

如果只是简单存储数据(如配置项),用普通对象更简洁;如果是纯函数逻辑(如工具函数),用模块导出函数更合适。

七、总结:类的本质是"优雅的原型封装"

JS的类本质是原型继承的语法糖,但它让代码更简洁、封装性更好、继承更清晰,尤其适合大型项目的结构化开发。记住核心比喻:

  • 类 = 设计图纸;
  • 实例 = 按图纸造的产品;
  • 构造函数 = 初始化流程;
  • 实例方法 = 产品功能;
  • 静态属性 = 工厂工具;
  • 继承 = 子工厂继承父工厂。

掌握类的用法,能让你从"零散写对象"升级到"系统化设计代码",这也是前端工程师从入门到进阶的关键一步。

相关推荐
光影少年2 小时前
PostgreSQL数据库学习路线
数据库·学习·postgresql
wjykp2 小时前
part 3神经网络的学习
人工智能·神经网络·学习
阿蒙Amon3 小时前
JavaScript学习笔记:10.集合
javascript·笔记·学习
快撑死的鱼3 小时前
Llama-factory 详细学习笔记:第六章:DPO (直接偏好优化) 实战 (难点)
笔记·学习·llama
d111111111d3 小时前
连续形式PID和离散PID-详情学习-江科大(学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习
四维碎片3 小时前
【Qt】生产者-消费者模式学习笔记
笔记·qt·学习
立志成为大牛的小牛3 小时前
数据结构——五十九、冒泡排序(王道408)
数据结构·学习·程序人生·考研·算法
试着3 小时前
【VSCode+AI+测试】连接ai大模型
ide·人工智能·vscode·python·学习·编辑器·ai-test