JavaScript 面向对象最佳实践
介绍
JavaScript作为一种多范式编程语言,允许开发者使用面向对象的方式来组织代码。虽然JavaScript的面向对象实现与传统的类继承语言(如Java或C++)有所不同,但通过掌握一些最佳实践,我们可以编写出更加结构化、可维护和可扩展的代码。
本文将介绍JavaScript中面向对象编程的最佳实践,帮助你避免常见陷阱,提高代码质量。
使用类语法而不是原型继承(ES6+)
虽然JavaScript的面向对象实现基于原型链,但ES6引入的类语法使代码更加清晰易读。
不推荐的方式(原型继承):
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
};
const john = new Person('John', 30);
console.log(john.greet()); // 输出: Hello, my name is John and I am 30 years old.
推荐的方式(类语法):
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
const john = new Person('John', 30);
console.log(john.greet()); // 输出: Hello, my name is John and I am 30 years old.
类语法更易于阅读和理解,尤其是对于来自其他面向对象语言的开发者。
使用私有字段和方法(ES2022+)
在现代JavaScript中,我们可以使用#
前缀来声明类的私有字段和方法,提高封装性。
class BankAccount {
#balance = 0;
#password;
constructor(initialBalance, password) {
this.#balance = initialBalance;
this.#password = password;
}
#validatePassword(inputPassword) {
return this.#password === inputPassword;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount, password) {
if (!this.#validatePassword(password)) {
return 'Authentication failed';
}
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return amount;
}
return 'Insufficient funds';
}
getBalance(password) {
if (this.#validatePassword(password)) {
return this.#balance;
}
return 'Authentication failed';
}
}
const account = new BankAccount(1000, 'secret123');
console.log(account.deposit(500)); // 输出: true
console.log(account.getBalance('secret123')); // 输出: 1500
console.log(account.withdraw(300, 'secret123')); // 输出: 300
console.log(account.getBalance('secret123')); // 输出: 1200
console.log(account.getBalance('wrongpass')); // 输出: Authentication failed
// 下面的代码会抛出错误,因为不能直接访问私有字段
// console.log(account.#balance);
// console.log(account.#validatePassword('secret123'));
私有字段语法(#
前缀)是ES2022的特性。如果你需要支持较老的浏览器,可能需要使用Babel等工具转译,或采用约定的下划线前缀(如_balance
)代替,但后者并不提供真正的私有性。
组合优于继承
在JavaScript中,组合通常比继承更灵活。可以通过对象组合实现代码重用,而不是依赖深层的继承层次结构。
不推荐过度使用继承:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
return `${this.name} is eating.`;
}
}
class Bird extends Animal {
fly() {
return `${this.name} is flying.`;
}
}
class Penguin extends Bird {
fly() {
return `${this.name} can't fly!`; // 覆盖父类方法,这表明继承层次不合理
}
swim() {
return `${this.name} is swimming.`;
}
}
const penguin = new Penguin('Pablo');
console.log(penguin.eat()); // 输出: Pablo is eating.
console.log(penguin.fly()); // 输出: Pablo can't fly!
console.log(penguin.swim()); // 输出: Pablo is swimming.
推荐使用组合:
// 功能混合器
const eater = (state) => ({
eat: () => `${state.name} is eating.`
});
const swimmer = (state) => ({
swim: () => `${state.name} is swimming.`
});
const flyer = (state) => ({
fly: () => `${state.name} is flying.`
});
// 创建对象
function createPenguin(name) {
const state = { name };
return {
...state,
...eater(state),
...swimmer(state)
};
}
function createSparrow(name) {
const state = { name };
return {
...state,
...eater(state),
...flyer(state)
};
}
const penguin = createPenguin('Pablo');
console.log(penguin.eat()); // 输出: Pablo is eating.
console.log(penguin.swim()); // 输出: Pablo is swimming.
// penguin.fly() 不存在,这很合理,因为企鹅不会飞
const sparrow = createSparrow('Jack');
console.log(sparrow.eat()); // 输出: Jack is eating.
console.log(sparrow.fly()); // 输出: Jack is flying.
// sparrow.swim() 不存在,这也很合理
组合方法让我们更自由地选择哪些功能应该包含在对象中,避免了继承可能带来的方法冲突和设计不合理问题。
不要修改内置对象的原型
修改JavaScript内置对象(如Array、String等)的原型可能导致不可预见的后果,包括与第三方库的冲突。
不推荐:
// 不要这样做!
Array.prototype.first = function() {
return this[0];
};
const arr = [1, 2, 3];
console.log(arr.first()); // 输出: 1
推荐的替代方法:
// 创建自己的工具函数
const arrayUtils = {
first: (array) => array[0]
};
const arr = [1, 2, 3];
console.log(arrayUtils.first(arr)); // 输出: 1
// 或者使用ES6的类继承
class MyArray extends Array {
first() {
return this[0];
}
}
const myArr = new MyArray(1, 2, 3);
console.log(myArr.first()); // 输出: 1
使用工厂函数创建对象
工厂函数是一种用于创建对象的函数,它可以封装对象创建逻辑,提高代码可维护性。
// 工厂函数
function createUser(username, email) {
const validateEmail = (email) => {
const re = /\S+@\S+\.\S+/;
return re.test(email);
};
if (!validateEmail(email)) {
throw new Error('Invalid email address');
}
return {
username,
email,
createdAt: new Date(),
isActive: true,
getInfo() {
return `User: ${this.username}, Email: ${this.email}, Active: ${this.isActive}`;
},
deactivate() {
this.isActive = false;
}
};
}
try {
const user1 = createUser('john_doe', 'john@example.com');
console.log(user1.getInfo());
// 输出: User: john_doe, Email: john@example.com, Active: true
user1.deactivate();
console.log(user1.getInfo());
// 输出: User: john_doe, Email: john@example.com, Active: false
// 会抛出错误
const invalidUser = createUser('invalid_user', 'not-an-email');
} catch (error) {
console.log(error.message); // 输出: Invalid email address
}
工厂函数不需要使用new
关键字,避免了忘记使用new
可能导致的错误,同时也提供了封装私有变量和方法的能力。
实际应用案例:电子商务购物车
下面是一个使用JavaScript面向对象最佳实践实现的购物车系统示例:
// Product类表示商品
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
getFormattedPrice() {
return `$${this.price.toFixed(2)}`;
}
}
// CartItem类表示购物车中的项目
class CartItem {
#product;
#quantity;
constructor(product, quantity = 1) {
this.#product = product;
this.#quantity = quantity;
}
get product() {
return this.#product;
}
get quantity() {
return this.#quantity;
}
set quantity(value) {
if (value < 1) {
throw new Error('Quantity cannot be less than 1');
}
this.#quantity = value;
}
get total() {
return this.#product.price * this.#quantity;
}
}
// ShoppingCart类管理购物车功能
class ShoppingCart {
#items = [];
#discountRate = 0;
addItem(product, quantity = 1) {
const existingItem = this.#findItem(product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.#items.push(new CartItem(product, quantity));
}
}
removeItem(productId) {
const index = this.#items.findIndex(item => item.product.id === productId);
if (index !== -1) {
this.#items.splice(index, 1);
return true;
}
return false;
}
updateQuantity(productId, quantity) {
const item = this.#findItem(productId);
if (item) {
item.quantity = quantity;
return true;
}
return false;
}
#findItem(productId) {
return this.#items.find(item => item.product.id === productId);
}
applyDiscount(rate) {
if (rate < 0 || rate > 1) {
throw new Error('Discount rate must be between 0 and 1');
}
this.#discountRate = rate;
}
get items() {
// 返回副本,防止外部修改内部状态
return [...this.#items];
}
get total() {
const subtotal = this.#items.reduce((sum, item) => sum + item.total, 0);
return subtotal * (1 - this.#discountRate);
}
get formattedTotal() {
return `$${this.total.toFixed(2)}`;
}
clear() {
this.#items = [];
this.#discountRate = 0;
}
checkout() {
// 在实际应用中,这里会处理支付流程
const orderSummary = {
items: this.items.map(item => ({
product: item.product.name,
price: item.product.price,
quantity: item.quantity,
itemTotal: item.total
})),
discount: this.#discountRate * 100 + '%',
total: this.total
};
this.clear();
return orderSummary;
}
}
// 使用示例
const laptop = new Product(1, 'Laptop', 999.99);
const mouse = new Product(2, 'Wireless Mouse', 29.99);
const keyboard = new Product(3, 'Mechanical Keyboard', 89.99);
const cart = new ShoppingCart();
cart.addItem(laptop);
cart.addItem(mouse, 2);
cart.addItem(keyboard);
console.log(`Items in cart: ${cart.items.length}`);
console.log(`Total: ${cart.formattedTotal}`);
// 更新鼠标数量
cart.updateQuantity(2, 3);
console.log(`New total after updating quantity: ${cart.formattedTotal}`);
// 应用折扣
cart.applyDiscount(0.1); // 10% 折扣
console.log(`Total after 10% discount: ${cart.formattedTotal}`);
// 结账
const order = cart.checkout();
console.log('Order summary:', order);
console.log(`Items in cart after checkout: ${cart.items.length}`);
这个购物车系统演示了多个面向对象最佳实践:
- 使用类语法定义对象
- 使用私有字段和方法提高封装性
- 通过getter和setter控制属性访问
- 返回对象副本而非直接引用,防止外部修改内部状态
- 单一职责原则:每个类只负责一个功能领域
总结
本文介绍了JavaScript面向对象编程的多种最佳实践:
- 使用ES6+类语法:比原型继承更易于理解和维护
- 使用私有字段和方法:提高封装性,保护内部实现
- 组合优于继承:采用组合方式可以提高代码灵活性
- 不修改内置对象原型:避免产生意外副作用
- 使用工厂函数:灵活创建对象,实现封装
- 保持单一职责:每个类只负责一个功能领域
遵循这些最佳实践,将帮助你编写出更加健壮、可维护和可扩展的JavaScript面向对象代码。
练习与资源
练习
- 创建一个
TodoList
类,实现添加、删除、标记完成等功能,使用私有字段存储任务列表。 - 使用组合模式设计一个角色扮演游戏中的角色系统,角色可以有不同的能力(如战斗、施法、治疗等)。
- 重构给定的原型继承代码,转换为使用ES6类语法。
进一步学习资源
- MDN Web Docs: Classes
- JavaScript: The Good Parts by Douglas Crockford
- You Don't Know JS: this & Object Prototypes
- Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides