跳到主要内容

ES6+ 类(Class)

类是 ES6 引入的重要语法,它为 JavaScript 提供了更清晰、更面向对象的方式来创建对象和管理继承。虽然 JavaScript 的类本质上仍然是基于原型的继承机制,但类语法让代码更加直观和易于理解。

类的本质

在深入类语法之前,我们需要理解一个重要概念:JavaScript 的类实际上是"特殊的函数"。类只是原型继承的语法糖,它并没有引入新的面向对象继承模型。

class Person {
constructor(name) {
this.name = name;
}

sayHello() {
console.log(`你好,我是${this.name}`);
}
}

typeof Person;

运行结果是 function,这证明了类本质上就是函数。但类与普通函数有一些重要区别:

  • 类必须使用 new 关键字调用,不能像普通函数那样直接执行
  • 类声明不会被提升,存在暂时性死区
  • 类内部自动运行在严格模式下

定义类的方式

JavaScript 提供两种定义类的方式:类声明和类表达式。

类声明

使用 class 关键字直接声明一个类:

class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}

getArea() {
return this.height * this.width;
}
}

const rect = new Rectangle(10, 5);
console.log(rect.getArea());

类表达式

类表达式可以将类赋值给变量,类可以是匿名的,也可以有名字:

const Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

const Circle = class CircleClass {
constructor(radius) {
this.radius = radius;
}

getArea() {
return Math.PI * this.radius * this.radius;
}
};

类表达式中,类名(如 CircleClass)只在类内部可见,外部只能通过变量名(Circle)访问。

构造函数

constructor 是类的特殊方法,用于创建和初始化对象实例。每个类只能有一个 constructor 方法。

class User {
constructor(username, email, age) {
this.username = username;
this.email = email;
this.age = age;
this.createdAt = new Date();
}

getInfo() {
return `${this.username} (${this.email})`;
}
}

const user = new User('张三', '[email protected]', 25);
console.log(user.getInfo());

如果类中没有显式定义 constructor,JavaScript 会自动添加一个空的构造函数。

构造函数中的默认值

可以为构造函数参数设置默认值:

class Product {
constructor(name, price = 0, quantity = 1) {
this.name = name;
this.price = price;
this.quantity = quantity;
}

getTotal() {
return this.price * this.quantity;
}
}

const product1 = new Product('笔记本', 5999);
const product2 = new Product('鼠标', 99, 3);

console.log(product1.getTotal());
console.log(product2.getTotal());

实例属性和方法

实例属性

实例属性是每个对象实例独有的属性,通常在构造函数中定义:

class Counter {
constructor(initialValue = 0) {
this.value = initialValue;
this.history = [];
}

increment() {
this.value++;
this.history.push(`+1 -> ${this.value}`);
}

decrement() {
this.value--;
this.history.push(`-1 -> ${this.value}`);
}
}

const counter = new Counter(10);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.value);
console.log(counter.history);

公有字段声明

ES2022 引入了公有字段声明语法,可以在类体顶部声明属性,使代码更清晰:

class Person {
name;
age = 0;
gender = '未知';

constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}

const person = new Person('李四', 30, '男');
console.log(person.name);
console.log(person.age);
console.log(person.gender);

这种写法的优势在于:所有属性一目了然,代码更具自文档性。

实例方法

实例方法定义在类的原型上,所有实例共享同一个方法:

class Calculator {
add(a, b) {
return a + b;
}

subtract(a, b) {
return a - b;
}

multiply(a, b) {
return a * b;
}

divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
}

const calc = new Calculator();
console.log(calc.add(5, 3));
console.log(calc.multiply(4, 7));

静态成员

静态成员属于类本身,而不是实例。使用 static 关键字定义。

静态方法

静态方法通常用于工具函数或不需要实例就能调用的方法:

class MathUtils {
static PI = 3.14159;

static square(x) {
return x * x;
}

static cube(x) {
return x * x * x;
}

static distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
}

console.log(MathUtils.square(5));
console.log(MathUtils.cube(3));
console.log(MathUtils.distance(0, 0, 3, 4));
console.log(MathUtils.PI);

注意:静态方法不能通过实例调用,只能通过类名调用。

const utils = new MathUtils();
utils.square(5);

静态方法的应用场景

静态方法常用于工厂方法、类型判断等场景:

class User {
constructor(name, role) {
this.name = name;
this.role = role;
}

static createAdmin(name) {
return new User(name, 'admin');
}

static createGuest() {
return new User('访客', 'guest');
}

static isAdmin(user) {
return user.role === 'admin';
}
}

const admin = User.createAdmin('管理员');
const guest = User.createGuest();

console.log(admin.name);
console.log(guest.name);
console.log(User.isAdmin(admin));
console.log(User.isAdmin(guest));

静态初始化块

ES2022 引入了静态初始化块,用于复杂的静态属性初始化:

class Config {
static settings = {};

static {
try {
const envData = process.env.NODE_ENV || 'development';
this.settings.environment = envData;
this.settings.debug = envData === 'development';
this.settings.version = '1.0.0';
} catch (error) {
console.error('配置初始化失败:', error);
}
}

static get(key) {
return this.settings[key];
}
}

console.log(Config.get('environment'));
console.log(Config.get('debug'));

Getter 和 Setter

Getter 和 Setter 用于控制属性的访问和修改,实现数据的封装。

基本用法

class Temperature {
constructor(celsius) {
this._celsius = celsius;
}

get celsius() {
return this._celsius;
}

set celsius(value) {
if (value < -273.15) {
throw new Error('温度不能低于绝对零度');
}
this._celsius = value;
}

get fahrenheit() {
return this._celsius * 9 / 5 + 32;
}

set fahrenheit(value) {
this._celsius = (value - 32) * 5 / 9;
}
}

const temp = new Temperature(25);
console.log(temp.celsius);
console.log(temp.fahrenheit);

temp.fahrenheit = 100;
console.log(temp.celsius);

数据验证和计算属性

Getter 和 Setter 常用于数据验证和创建计算属性:

class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}

get fullName() {
return `${this._firstName} ${this._lastName}`;
}

set fullName(value) {
const parts = value.split(' ');
if (parts.length !== 2) {
throw new Error('请输入完整的姓名(名 姓)');
}
this._firstName = parts[0];
this._lastName = parts[1];
}

get initials() {
return this._firstName[0] + this._lastName[0];
}
}

const person = new Person('张', '三');
console.log(person.fullName);
console.log(person.initials);

person.fullName = '李 四';
console.log(person.fullName);

私有成员

ES2022 引入了私有字段和方法,使用 # 前缀标识。私有成员只能在类内部访问。

私有字段

class BankAccount {
#balance = 0;
#pin;

constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
}

deposit(amount) {
if (amount <= 0) {
throw new Error('存款金额必须大于零');
}
this.#balance += amount;
return this.#balance;
}

withdraw(amount, pin) {
if (pin !== this.#pin) {
throw new Error('密码错误');
}
if (amount > this.#balance) {
throw new Error('余额不足');
}
this.#balance -= amount;
return this.#balance;
}

getBalance(pin) {
if (pin !== this.#pin) {
throw new Error('密码错误');
}
return this.#balance;
}
}

const account = new BankAccount(1000, '1234');
account.deposit(500);
console.log(account.getBalance('1234'));
account.withdraw(200, '1234');
console.log(account.getBalance('1234'));

尝试从外部访问私有字段会报错:

console.log(account.#balance);

私有方法

私有方法用于隐藏内部实现细节:

class DataProcessor {
#data = [];

constructor(data) {
this.#data = data;
}

process() {
const cleaned = this.#cleanData();
const sorted = this.#sortData(cleaned);
return this.#calculateStats(sorted);
}

#cleanData() {
return this.#data.filter(item => item !== null && item !== undefined);
}

#sortData(data) {
return [...data].sort((a, b) => a - b);
}

#calculateStats(data) {
if (data.length === 0) return null;

const sum = data.reduce((acc, val) => acc + val, 0);
const avg = sum / data.length;
const min = Math.min(...data);
const max = Math.max(...data);

return { sum, avg, min, max, count: data.length };
}
}

const processor = new DataProcessor([3, null, 1, 5, undefined, 2, 4]);
console.log(processor.process());

继承

使用 extends 关键字实现类的继承,子类可以继承父类的属性和方法。

基本继承

class Animal {
constructor(name) {
this.name = name;
}

speak() {
console.log(`${this.name} 发出声音`);
}

move() {
console.log(`${this.name} 移动`);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

speak() {
console.log(`${this.name} 汪汪叫`);
}

fetch() {
console.log(`${this.name} 去捡球`);
}
}

class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}

speak() {
console.log(`${this.name} 喵喵叫`);
}

climb() {
console.log(`${this.name} 爬树`);
}
}

const dog = new Dog('旺财', '金毛');
const cat = new Cat('咪咪', '橘色');

dog.speak();
dog.fetch();

cat.speak();
cat.climb();

super 关键字

super 关键字用于调用父类的构造函数或方法:

class Vehicle {
constructor(brand, speed) {
this.brand = brand;
this.speed = speed;
}

accelerate(amount) {
this.speed += amount;
console.log(`${this.brand} 加速到 ${this.speed} km/h`);
}

brake(amount) {
this.speed = Math.max(0, this.speed - amount);
console.log(`${this.brand} 减速到 ${this.speed} km/h`);
}
}

class Car extends Vehicle {
constructor(brand, speed, doors) {
super(brand, speed);
this.doors = doors;
}

accelerate(amount) {
console.log('汽车加速中...');
super.accelerate(amount);
}

honk() {
console.log(`${this.brand} 滴滴滴!`);
}
}

const car = new Car('丰田', 0, 4);
car.accelerate(50);
car.brake(20);
car.honk();

继承链

子类可以继续被继承,形成继承链:

class Shape {
constructor(color) {
this.color = color;
}

describe() {
return `这是一个${this.color}的形状`;
}
}

class Rectangle extends Shape {
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}

getArea() {
return this.width * this.height;
}

describe() {
return `${super.describe()},宽${this.width},高${this.height},面积${this.getArea()}`;
}
}

class Square extends Rectangle {
constructor(color, side) {
super(color, side, side);
this.side = side;
}

describe() {
return `${super.describe()},这是一个正方形`;
}
}

const square = new Square('红色', 5);
console.log(square.describe());
console.log(square.getArea());

类的 this 绑定

类方法中的 this 绑定有一些特殊行为需要注意。

方法解绑问题

当把类方法单独提取出来调用时,this 会丢失:

class Logger {
constructor(prefix) {
this.prefix = prefix;
}

log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}

const logger = new Logger('APP');
logger.log('正常调用');

const logFn = logger.log;
logFn('解绑调用');

解决方案

有几种方式可以解决 this 绑定问题:

方法一:使用箭头函数字段

class Logger {
constructor(prefix) {
this.prefix = prefix;
}

log = (message) => {
console.log(`[${this.prefix}] ${message}`);
};
}

const logger = new Logger('APP');
const logFn = logger.log;
logFn('箭头函数调用');

方法二:在构造函数中绑定

class Logger {
constructor(prefix) {
this.prefix = prefix;
this.log = this.log.bind(this);
}

log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}

const logger = new Logger('APP');
const logFn = logger.log;
logFn('bind 绑定调用');

方法三:调用时绑定

class Logger {
constructor(prefix) {
this.prefix = prefix;
}

log(message) {
console.log(`[${this.prefix}] ${message}`);
}
}

const logger = new Logger('APP');
const logFn = logger.log.bind(logger);
logFn('手动绑定调用');

实际应用示例

实现一个简单的事件发射器

class EventEmitter {
#events = new Map();

on(event, listener) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(listener);
return this;
}

off(event, listener) {
if (!this.#events.has(event)) return this;

const listeners = this.#events.get(event);
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
return this;
}

emit(event, ...args) {
if (!this.#events.has(event)) return false;

const listeners = this.#events.get(event);
listeners.forEach(listener => listener(...args));
return true;
}

once(event, listener) {
const onceWrapper = (...args) => {
listener(...args);
this.off(event, onceWrapper);
};
return this.on(event, onceWrapper);
}
}

const emitter = new EventEmitter();

emitter.on('message', (msg) => {
console.log(`收到消息: ${msg}`);
});

emitter.once('connect', () => {
console.log('已连接(只触发一次)');
});

emitter.emit('message', '你好');
emitter.emit('connect');
emitter.emit('connect');
emitter.emit('message', '世界');

实现一个链表数据结构

class ListNode {
constructor(value) {
this.value = value;
this.next = null;
}
}

class LinkedList {
#head = null;
#tail = null;
#size = 0;

append(value) {
const node = new ListNode(value);
if (!this.#head) {
this.#head = node;
this.#tail = node;
} else {
this.#tail.next = node;
this.#tail = node;
}
this.#size++;
return this;
}

prepend(value) {
const node = new ListNode(value);
node.next = this.#head;
this.#head = node;
if (!this.#tail) {
this.#tail = node;
}
this.#size++;
return this;
}

get size() {
return this.#size;
}

get first() {
return this.#head?.value;
}

get last() {
return this.#tail?.value;
}

toArray() {
const result = [];
let current = this.#head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}

find(callback) {
let current = this.#head;
while (current) {
if (callback(current.value)) {
return current.value;
}
current = current.next;
}
return undefined;
}

remove(callback) {
if (!this.#head) return false;

if (callback(this.#head.value)) {
this.#head = this.#head.next;
if (!this.#head) this.#tail = null;
this.#size--;
return true;
}

let current = this.#head;
while (current.next) {
if (callback(current.next.value)) {
current.next = current.next.next;
if (!current.next) this.#tail = current;
this.#size--;
return true;
}
current = current.next;
}
return false;
}
}

const list = new LinkedList();
list.append(1).append(2).append(3);
list.prepend(0);

console.log(list.toArray());
console.log(list.size);
console.log(list.first);
console.log(list.last);

list.remove(x => x === 2);
console.log(list.toArray());

类与构造函数的对比

理解类与传统构造函数的对应关系有助于深入理解 JavaScript:

class Person {
constructor(name) {
this.name = name;
}

greet() {
console.log(`你好,我是${this.name}`);
}

static create(name) {
return new Person(name);
}
}

function PersonFunc(name) {
this.name = name;
}

PersonFunc.prototype.greet = function() {
console.log(`你好,我是${this.name}`);
};

PersonFunc.create = function(name) {
return new PersonFunc(name);
};

const p1 = new Person('张三');
const p2 = new PersonFunc('李四');

p1.greet();
p2.greet();

const p3 = Person.create('王五');
const p4 = PersonFunc.create('赵六');

p3.greet();
p4.greet();

小结

JavaScript 的类语法提供了更清晰、更结构化的方式来创建对象和组织代码。掌握类的核心概念对于现代 JavaScript 开发至关重要:

  • 类是原型继承的语法糖,本质上是特殊的函数
  • constructor 用于初始化实例,每个类只能有一个
  • 静态成员属于类本身,实例成员属于每个对象实例
  • Getter 和 Setter 提供了属性访问的控制能力
  • 私有成员(#前缀)实现了真正的数据封装
  • extendssuper 实现了类继承
  • 注意方法解绑时的 this 问题,可以使用箭头函数或 bind 解决

参考资料