跳到主要内容

JavaScript 重构技巧

什么是代码重构?

重构是指在不改变代码外部行为的前提下,对代码内部结构进行调整,使其更加清晰、简洁和易于维护。就像整理你的房间一样,重构不会改变房间的功能,但会让你更容易找到东西,也更舒适。

提示

好的重构可以降低技术债务,提高代码质量,并为未来的开发节省时间!

为什么需要重构?

  • 提高代码可读性:让其他开发者(包括未来的你)更容易理解
  • 简化维护:更容易找到并修复bug
  • 提高性能:有时重构可以带来性能提升
  • 便于扩展:使添加新功能更加容易

基本重构技巧

1. 提取函数

当一个函数过长或者一段代码可以被抽象为独立功能时,可以提取为单独的函数。

重构前:

javascript
function calculateAndPrintInvoice(items) {
let total = 0;

// 计算总金额
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}

// 应用折扣
if (total > 100) {
total *= 0.9; // 10%折扣
}

// 打印发票
console.log('--- 发票 ---');
for (let i = 0; i < items.length; i++) {
console.log(`${items[i].name}: ${items[i].price} x ${items[i].quantity}`);
}
console.log(`总计: ${total}`);
}

重构后:

javascript
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}

function applyDiscount(total) {
if (total > 100) {
return total * 0.9; // 10%折扣
}
return total;
}

function printInvoice(items, total) {
console.log('--- 发票 ---');
for (let i = 0; i < items.length; i++) {
console.log(`${items[i].name}: ${items[i].price} x ${items[i].quantity}`);
}
console.log(`总计: ${total}`);
}

function calculateAndPrintInvoice(items) {
const rawTotal = calculateTotal(items);
const finalTotal = applyDiscount(rawTotal);
printInvoice(items, finalTotal);
}

2. 有意义的命名

变量和函数名应该清晰地表达其用途,这样可以大大提高代码可读性。

重构前:

javascript
function fn(a, t) {
const x = a.filter(i => i.s === t);
return x;
}

重构后:

javascript
function filterItemsByStatus(items, statusType) {
const filteredItems = items.filter(item => item.status === statusType);
return filteredItems;
}

3. 使用现代JavaScript语法

ES6及以后的JavaScript版本提供了许多简化代码的语法特性。

重构前:

javascript
var users = ['Alice', 'Bob', 'Charlie'];
var userObjects = [];

for (var i = 0; i < users.length; i++) {
userObjects.push({
name: users[i],
index: i
});
}

function greet(user) {
return 'Hello, ' + user.name + '!';
}

重构后:

javascript
const users = ['Alice', 'Bob', 'Charlie'];
const userObjects = users.map((name, index) => ({ name, index }));

const greet = user => `Hello, ${user.name}!`;

中级重构技巧

1. 替换条件表达式为对象映射

当有多个条件分支执行不同代码时,可以使用对象映射替代复杂的if/else或switch语句。

重构前:

javascript
function getDiscountRate(customerType) {
if (customerType === 'regular') {
return 0.1;
} else if (customerType === 'premium') {
return 0.2;
} else if (customerType === 'vip') {
return 0.3;
} else {
return 0;
}
}

重构后:

javascript
function getDiscountRate(customerType) {
const discountRates = {
regular: 0.1,
premium: 0.2,
vip: 0.3
};

return discountRates[customerType] || 0;
}

2. 分解复杂条件

当条件表达式变得复杂时,将其提取为单独的函数可以提高可读性。

重构前:

javascript
if (date.before(SUMMER_START) || date.after(SUMMER_END) 
&& !user.isSpecial() && user.getSubscription().isActive()) {
applyExtraCharge();
}

重构后:

javascript
function isEligibleForExtraCharge(date, user) {
const isOutsideSummer = date.before(SUMMER_START) || date.after(SUMMER_END);
const isRegularActiveUser = !user.isSpecial() && user.getSubscription().isActive();

return isOutsideSummer && isRegularActiveUser;
}

if (isEligibleForExtraCharge(date, user)) {
applyExtraCharge();
}

3. 移除重复代码

DRY原则(Don't Repeat Yourself)是重构的关键原则之一。

重构前:

javascript
function showUserProfile(user) {
const name = user.firstName + ' ' + user.lastName;
const formattedDate = new Date(user.birthDate).toLocaleDateString();
// 显示用户信息...
}

function printUserCard(user) {
const name = user.firstName + ' ' + user.lastName;
const formattedDate = new Date(user.birthDate).toLocaleDateString();
// 打印用户卡片...
}

重构后:

javascript
function formatUserName(user) {
return user.firstName + ' ' + user.lastName;
}

function formatDate(dateString) {
return new Date(dateString).toLocaleDateString();
}

function showUserProfile(user) {
const name = formatUserName(user);
const formattedDate = formatDate(user.birthDate);
// 显示用户信息...
}

function printUserCard(user) {
const name = formatUserName(user);
const formattedDate = formatDate(user.birthDate);
// 打印用户卡片...
}

高级重构技巧

1. 使用策略模式

当有多种算法或策略可以互换使用时,可以使用策略模式,使它们可以自由替换。

重构前:

javascript
function calculateShipping(cart, shippingMethod) {
if (shippingMethod === 'standard') {
return cart.getTotalWeight() * 0.5;
} else if (shippingMethod === 'express') {
return 10 + (cart.getTotalWeight() * 1);
} else if (shippingMethod === 'international') {
return 20 + (cart.getTotalWeight() * 2);
}
}

重构后:

javascript
const shippingStrategies = {
standard: cart => cart.getTotalWeight() * 0.5,
express: cart => 10 + (cart.getTotalWeight() * 1),
international: cart => 20 + (cart.getTotalWeight() * 2)
};

function calculateShipping(cart, shippingMethod) {
return shippingStrategies[shippingMethod](cart);
}

2. 使用组合替代继承

在某些场景下,使用组合比使用继承更灵活。

重构前:

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

eat() {
console.log(`${this.name} 在吃东西`);
}
}

class Bird extends Animal {
fly() {
console.log(`${this.name} 在飞`);
}
}

class Fish extends Animal {
swim() {
console.log(`${this.name} 在游泳`);
}
}

重构后:

javascript
const eater = name => ({
eat: () => console.log(`${name} 在吃东西`)
});

const flyer = name => ({
fly: () => console.log(`${name} 在飞`)
});

const swimmer = name => ({
swim: () => console.log(`${name} 在游泳`)
});

function createBird(name) {
return {
name,
...eater(name),
...flyer(name)
};
}

function createFish(name) {
return {
name,
...eater(name),
...swimmer(name)
};
}

// 现在我们也可以轻松创建一个既会飞又会游泳的动物
function createFlyingFish(name) {
return {
name,
...eater(name),
...flyer(name),
...swimmer(name)
};
}

实际项目中的重构案例

案例:重构一个表单验证模块

重构前:

javascript
function validateForm() {
// 验证用户名
const username = document.getElementById('username').value;
if (username.length < 3) {
alert('用户名必须至少3个字符');
return false;
}

// 验证密码
const password = document.getElementById('password').value;
if (password.length < 6) {
alert('密码必须至少6个字符');
return false;
}

// 验证邮箱
const email = document.getElementById('email').value;
if (!email.includes('@') || !email.includes('.')) {
alert('请输入有效的邮箱地址');
return false;
}

return true;
}

重构后:

javascript
// 验证器集合
const validators = {
username: {
validate: value => value.length >= 3,
message: '用户名必须至少3个字符'
},
password: {
validate: value => value.length >= 6,
message: '密码必须至少6个字符'
},
email: {
validate: value => value.includes('@') && value.includes('.'),
message: '请输入有效的邮箱地址'
}
};

// 获取表单字段值
function getFieldValue(fieldId) {
return document.getElementById(fieldId).value;
}

// 验证单个字段
function validateField(fieldId) {
const value = getFieldValue(fieldId);
const validator = validators[fieldId];

if (!validator.validate(value)) {
displayError(validator.message);
return false;
}

return true;
}

// 显示错误信息
function displayError(message) {
alert(message);
}

// 主验证函数
function validateForm() {
const fields = ['username', 'password', 'email'];

for (const field of fields) {
if (!validateField(field)) {
return false;
}
}

return true;
}

这个重构版本带来的好处:

  1. 可扩展性:轻松添加新的验证字段和规则
  2. 可测试性:每个函数都有单一职责,易于测试
  3. 可维护性:验证规则和错误消息集中管理
  4. 代码重用:验证逻辑被抽象,可以在应用的其他部分重用

重构最佳实践

  1. 小步前进:每次只做一个小改动,然后测试确保功能正常
  2. 保持测试:有单元测试会大大降低重构风险
  3. 频繁提交:每完成一个小的重构,就提交代码,便于回退
  4. 保持代码风格一致:遵循团队的代码规范
  5. 先清理再添加功能:不要同时重构和添加新功能

重构工具和资源

  • IDE支持:现代IDE(如VS Code、WebStorm)提供了强大的重构工具
  • ESLint:可以自动检测和修复一些常见问题
  • Prettier:自动格式化代码,保持一致的风格
  • 测试框架:如Jest,帮助确保重构不破坏现有功能

总结

重构是一项需要持续练习的技能,它帮助我们不断改进代码质量,而不必重写整个系统。通过本文介绍的技巧,你可以开始在自己的JavaScript项目中应用重构,使代码更加清晰、简洁和可维护。

记住,好的重构就像园丁修剪花园,不是一次性大改动,而是持续的、小心的维护工作。随着经验的积累,你会发现重构不仅能提高代码质量,还能提升你对代码的理解和掌控能力。

警告

在没有测试覆盖的情况下对生产代码进行大规模重构是非常危险的!

练习题

  1. 找出你的一个项目中最长的函数,尝试使用"提取函数"技巧将其分解为更小的函数
  2. 寻找代码中的重复逻辑,将其提取为可重用函数
  3. 使用对象映射替换一个复杂的switch或if-else结构
  4. 尝试改进一些变量和函数的命名,使其更加清晰和具描述性

进一步学习资源

  • 《重构:改善既有代码的设计》- Martin Fowler
  • 《代码整洁之道》- Robert C. Martin
  • 《JavaScript设计模式》- Addy Osmani

实践是掌握重构技巧的关键,所以不要犹豫,勇敢地开始改进你的代码吧!