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