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 提供了属性访问的控制能力
- 私有成员(
#前缀)实现了真正的数据封装 extends和super实现了类继承- 注意方法解绑时的
this问题,可以使用箭头函数或bind解决