ES6类语法
2025年3月7日大约 9 分钟
ES6类语法
ES6引入的class关键字提供了定义类的新语法,使JavaScript的面向对象编程更加清晰和直观。本文将详细介绍类的定义、构造函数、实例方法和访问器属性等基本语法。
类的基本语法
类的定义
使用class
关键字可以定义一个类:
class Person {
// 类的内容
}
类声明不会被提升,这意味着必须先声明类,然后才能使用它:
// 错误:ReferenceError
const p = new Person();
// 正确的类声明顺序
class Person {}
const p = new Person();
类也可以使用表达式的方式定义:
// 匿名类表达式
const Person = class {
// 类的内容
};
// 命名类表达式
const Person = class PersonClass {
// 类的内容
};
构造函数
构造函数constructor
是类中的特殊方法,用于创建和初始化类的实例:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person = new Person('张三', 30);
console.log(person.name); // 输出:张三
console.log(person.age); // 输出:30
如果没有显式定义构造函数,JavaScript会自动添加一个空的构造函数:
class Person {
// 自动添加:constructor() {}
}
实例方法
在类中定义的方法会成为类原型上的方法,被所有实例共享:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`你好,我是${this.name},今年${this.age}岁`);
}
getAge() {
return this.age;
}
}
const person = new Person('张三', 30);
person.sayHello(); // 输出:你好,我是张三,今年30岁
console.log(person.getAge()); // 输出:30
访问器属性(Getter和Setter)
类可以包含getter和setter方法,用于访问和修改类的属性:
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
// getter
get fullName() {
return `${this._lastName}${this._firstName}`;
}
// setter
set fullName(value) {
const parts = value.split(' ');
this._lastName = parts[0];
this._firstName = parts[1] || '';
}
}
const person = new Person('三', '张');
console.log(person.fullName); // 输出:张三
person.fullName = '李 四';
console.log(person._firstName); // 输出:四
console.log(person._lastName); // 输出:李
类的高级特性
静态方法和属性
静态方法和属性是属于类本身而不是类的实例的方法和属性:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
sayHello() {
console.log(`你好,我是${this.name}`);
}
// 静态方法
static createAnonymous() {
return new Person('匿名用户', 0);
}
// 静态属性(ES2022+)
static species = '人类';
}
// 调用静态方法
const anonymous = Person.createAnonymous();
anonymous.sayHello(); // 输出:你好,我是匿名用户
// 访问静态属性
console.log(Person.species); // 输出:人类
在ES2022之前,静态属性需要在类定义外部添加:
class Person {
static createAnonymous() {
return new Person('匿名用户', 0);
}
}
// 在类定义外部添加静态属性
Person.species = '人类';
私有字段和方法
从ES2022开始,JavaScript支持使用#
前缀定义私有字段和方法:
class Person {
// 私有字段
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
// 公共方法
introduce() {
console.log(`我是${this.#name},今年${this.#age}岁`);
this.#privateMethod();
}
// 私有方法
#privateMethod() {
console.log('这是一个私有方法');
}
// 访问私有字段的公共方法
getName() {
return this.#name;
}
setName(name) {
this.#name = name;
}
}
const person = new Person('张三', 30);
person.introduce(); // 输出:我是张三,今年30岁 和 这是一个私有方法
console.log(person.getName()); // 输出:张三
person.setName('李四');
console.log(person.getName()); // 输出:李四
// 错误:无法直接访问私有字段
// console.log(person.#name); // SyntaxError
// person.#privateMethod(); // SyntaxError
计算属性名
类中的方法和访问器属性可以使用计算的属性名:
const methodName = 'sayHello';
const getterName = 'fullName';
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// 计算方法名
[methodName]() {
console.log(`你好,我是${this.firstName}`);
}
// 计算的getter名
get [getterName]() {
return `${this.lastName}${this.firstName}`;
}
}
const person = new Person('三', '张');
person.sayHello(); // 输出:你好,我是三
console.log(person.fullName); // 输出:张三
生成器方法
类中可以定义生成器方法:
class NumberSequence {
constructor(start = 0, end = Infinity, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
// 生成器方法
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i += this.step) {
yield i;
}
}
}
const numbers = new NumberSequence(1, 10, 2);
for (const num of numbers) {
console.log(num); // 输出:1, 3, 5, 7, 9
}
// 使用扩展运算符
console.log([...numbers]); // 输出:[1, 3, 5, 7, 9]
类与构造函数的对比
ES6的类本质上是JavaScript现有基于原型继承的语法糖。以下是类与传统构造函数的对比:
使用类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`你好,我是${this.name}`);
}
}
const person = new Person('张三', 30);
person.sayHello(); // 输出:你好,我是张三
使用构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const person = new Person('张三', 30);
person.sayHello(); // 输出:你好,我是张三
主要区别
- 语法更清晰:类语法更加简洁和直观。
- 强制使用new:类必须使用
new
关键字调用,而构造函数如果不使用new
会将属性添加到全局对象。 - 不可枚举的方法:类的方法默认是不可枚举的,而构造函数原型上的方法是可枚举的。
- 严格模式:类内部代码自动运行在严格模式下。
- 不存在提升:类声明不会被提升,而函数声明会被提升。
类的实际应用
创建UI组件
class Tooltip {
constructor(element, text) {
this.element = element;
this.text = text;
this.tooltip = null;
// 绑定事件
this.element.addEventListener('mouseenter', this.show.bind(this));
this.element.addEventListener('mouseleave', this.hide.bind(this));
}
show() {
// 创建tooltip元素
this.tooltip = document.createElement('div');
this.tooltip.className = 'tooltip';
this.tooltip.textContent = this.text;
// 计算位置
const rect = this.element.getBoundingClientRect();
this.tooltip.style.top = `${rect.bottom + 10}px`;
this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
// 添加到DOM
document.body.appendChild(this.tooltip);
}
hide() {
if (this.tooltip) {
document.body.removeChild(this.tooltip);
this.tooltip = null;
}
}
}
// 使用
const button = document.querySelector('button');
new Tooltip(button, '点击提交表单');
数据模型
class User {
#id;
#password;
constructor(id, name, email, password) {
this.#id = id;
this.name = name;
this.email = email;
this.#password = password;
this.createdAt = new Date();
}
get id() {
return this.#id;
}
updateEmail(newEmail) {
// 可以添加验证逻辑
this.email = newEmail;
}
checkPassword(password) {
return this.#password === password;
}
toJSON() {
// 返回可序列化的对象,排除私有字段
return {
id: this.#id,
name: this.name,
email: this.email,
createdAt: this.createdAt
};
}
static fromJSON(json) {
return new User(json.id, json.name, json.email, '');
}
}
// 使用
const user = new User(1, '张三', 'zhangsan@example.com', 'password123');
console.log(user.toJSON());
// 从服务器获取的数据创建用户
const userData = {
id: 2,
name: '李四',
email: 'lisi@example.com',
createdAt: '2023-01-01T00:00:00.000Z'
};
const serverUser = User.fromJSON(userData);
最佳实践
1. 使用私有字段保护数据
class BankAccount {
#balance = 0;
#transactions = [];
constructor(initialBalance = 0) {
if (initialBalance > 0) {
this.deposit(initialBalance);
}
}
deposit(amount) {
if (amount <= 0) {
throw new Error('存款金额必须大于0');
}
this.#balance += amount;
this.#transactions.push({
type: 'deposit',
amount,
date: new Date()
});
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) {
throw new Error('取款金额必须大于0');
}
if (amount > this.#balance) {
throw new Error('余额不足');
}
this.#balance -= amount;
this.#transactions.push({
type: 'withdraw',
amount,
date: new Date()
});
return this.#balance;
}
get balance() {
return this.#balance;
}
get transactionHistory() {
// 返回交易记录的副本,防止外部修改
return [...this.#transactions];
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.balance); // 输出:1300
console.log(account.transactionHistory);
2. 组合优于继承
// 功能类
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class EventEmitter {
#events = {};
on(event, listener) {
if (!this.#events[event]) {
this.#events[event] = [];
}
this.#events[event].push(listener);
return this;
}
emit(event, ...args) {
if (!this.#events[event]) return false;
this.#events[event].forEach(listener => listener(...args));
return true;
}
}
// 使用组合而非继承
class HttpClient {
constructor() {
this.logger = new Logger();
this.events = new EventEmitter();
}
async fetch(url) {
this.logger.log(`Fetching ${url}`);
this.events.emit('beforeFetch', url);
try {
const response = await fetch(url);
const data = await response.json();
this.events.emit('afterFetch', data);
return data;
} catch (error) {
this.logger.log(`Error: ${error.message}`);
this.events.emit('error', error);
throw error;
}
}
}
// 使用
const client = new HttpClient();
client.events.on('afterFetch', data => {
console.log('数据获取成功:', data);
});
client.fetch('https://api.example.com/users');
3. 使用工厂模式创建对象
class UserFactory {
static createAdmin(name, email) {
const user = new User(Date.now(), name, email, 'admin123');
user.role = 'admin';
user.permissions = ['read', 'write', 'delete'];
return user;
}
static createRegularUser(name, email) {
const user = new User(Date.now(), name, email, 'user123');
user.role = 'user';
user.permissions = ['read'];
return user;
}
static createGuestUser() {
const user = new User(Date.now(), 'Guest', 'guest@example.com', 'guest123');
user.role = 'guest';
user.permissions = ['read'];
user.expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
return user;
}
}
// 使用
const admin = UserFactory.createAdmin('管理员', 'admin@example.com');
const user = UserFactory.createRegularUser('普通用户', 'user@example.com');
const guest = UserFactory.createGuestUser();
4. 使用Mixin扩展类功能
// Mixin函数
const LoggerMixin = (Base) => class extends Base {
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
logError(error) {
console.error(`[${new Date().toISOString()}] Error: ${error.message}`);
}
};
const StorageMixin = (Base) => class extends Base {
save(key, data) {
localStorage.setItem(key, JSON.stringify(data));
}
load(key) {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
remove(key) {
localStorage.removeItem(key);
}
};
// 使用Mixin
class UserManager extends LoggerMixin(StorageMixin(class {})) {
constructor() {
super();
this.users = this.load('users') || [];
}
addUser(user) {
this.log(`Adding user: ${user.name}`);
this.users.push(user);
this.save('users', this.users);
}
removeUser(userId) {
this.log(`Removing user with ID: ${userId}`);
this.users = this.users.filter(user => user.id !== userId);
this.save('users', this.users);
}
}
// 使用
const userManager = new UserManager();
userManager.addUser({ id: 1, name: '张三' });
console.log(userManager.users);
类的常见陷阱和解决方案
1. this绑定问题
在类的方法中,this
的值取决于方法如何被调用:
class Button {
constructor(text) {
this.text = text;
this.element = document.createElement('button');
this.element.textContent = text;
// 错误方式:this会丢失
this.element.addEventListener('click', function() {
console.log(`按钮"${this.text}"被点击了`); // this指向element,而不是Button实例
});
// 正确方式1:使用箭头函数
this.element.addEventListener('click', () => {
console.log(`按钮"${this.text}"被点击了`); // 箭头函数不绑定自己的this
});
// 正确方式2:使用bind
this.element.addEventListener('click', this.handleClick.bind(this));
}
handleClick() {
console.log(`按钮"${this.text}"被点击了`);
}
// 正确方式3:使用类字段和箭头函数(推荐)
handleClickField = () => {
console.log(`按钮"${this.text}"被点击了`);
}
}
const button = new Button('提交');
document.body.appendChild(button.element);
2. 类的继承陷阱
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name}发出声音`);
}
}
class Dog extends Animal {
constructor(name, breed) {
// 错误:在super()之前使用this
// this.breed = breed; // ReferenceError
// 正确:先调用super()
super(name);
this.breed = breed;
}
speak() {
// 调用父类方法
super.speak();
console.log(`${this.name}汪汪叫`);
}
}
const dog = new Dog('小黑', '拉布拉多');
dog.speak();
// 输出:
// 小黑发出声音
// 小黑汪汪叫
3. 类字段初始化顺序
class Example {
// 1. 首先初始化类字段
counter = 0;
// 2. 然后执行构造函数
constructor() {
this.incrementCounter();
console.log(`构造函数中的counter: ${this.counter}`);
}
incrementCounter() {
this.counter++;
}
}
const example = new Example();
// 输出:构造函数中的counter: 1
总结
ES6类语法为JavaScript提供了更清晰、更直观的面向对象编程方式。通过本文,我们学习了:
- 类的基本语法:类的定义、构造函数、实例方法和访问器属性
- 类的高级特性:静态方法和属性、私有字段和方法、计算属性名和生成器方法
- 类与构造函数的对比:了解类本质上是基于原型继承的语法糖
- 类的实际应用:UI组件和数据模型的实现
- 最佳实践:使用私有字段保护数据、组合优于继承、工厂模式和Mixin模式
- 常见陷阱和解决方案:this绑定问题、继承陷阱和类字段初始化顺序
虽然JavaScript的类语法与传统面向对象语言(如Java或C#)有所不同,但它提供了一种更结构化的方式来组织代码,特别适合构建复杂的应用程序。随着私有字段等新特性的加入,JavaScript的类系统变得更加强大和完善。