代码规范与重构
代码规范解决的是"写得整齐",重构解决的是"写得更好"。两者共同的敌人是可读性与可维护性的下降。本文先建立一套可落地的命名与函数设计规范,再识别代码中的坏味道,最后给出对应的重构手法,并单独讨论异步代码的重构路径。
一、命名规范
命名的本质是降低阅读者的认知负担:看到名字,就应该知道它是什么、用来干什么。
1. 变量与常量命名
| 场景 | 命名风格 | 示例 |
|---|---|---|
| 局部变量 | camelCase,语义化名词 | userList、totalPrice |
| 常量(不会变) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| 布尔值 | is/has/can/should 前缀 | isLoading、hasPermission |
| 数组/集合 | 复数名词或 List 后缀 | items、users |
| Map/Set | 复数或带类型后缀 | userMap、visitedSet |
// 差:含义不明,靠上下文猜
const d = new Date();
const n = users.length;
const temp = config.proxy;
// 好:见名知意
const today = new Date();
const userCount = users.length;
const proxyConfig = config.proxy;
// 布尔变量用前缀表达判断语义
const isLoggedIn = !!token;
const hasChildren = node.children.length > 0;2. 函数命名:动词开头
函数代表动作,命名用动词 + 名词结构,并遵循统一的动词约定:
| 动词 | 语义约定 | 示例 |
|---|---|---|
get | 同步取值(不承诺一定有) | getUserName() |
fetch/load | 异步拉取(返回 Promise) | fetchUserDetail() |
create/add | 创建/新增 | createOrder() |
update/set | 更新/设置 | updateProfile() |
remove/delete | 删除 | removeItem() |
is/has/can | 返回布尔值的判断 | isValidEmail() |
handle | 事件处理回调 | handleClick() |
to | 类型转换 | toDateString() |
// 差:职责不明、无法预测返回值
function check(user) { /* 校验?还是查询?返回什么? */ }
// 好:动词清楚表达动作与返回值类型
function isUserAdult(user) {
return user.age >= 18;
}3. 类与构造函数:名词 + PascalCase
类表示一类事物,用名词命名,单词首字母大写;私有成员用 # 前缀。
class OrderService { // 名词 + Service,表示领域服务
#orders = new Map(); // 私有字段
getById(id) { /* ... */ }
}
class ShoppingCart { /* ... */ }命名自查清单:看到名字能回答三个问题——"它是什么?""它做什么?""它返回什么?"只要有一个回答不上来,就该改名。
二、函数设计原则
函数是 JavaScript 中最小的组织单元,下面五个原则直接决定代码质量。
1. 单一职责
一个函数只做一件事,并且把这件事做完。判断标准:能否用一句"它做什么"完整描述,描述中出现"并且"就要拆。
// 差:一个函数做了三件事
function processOrder(order) {
order.total = order.items.reduce((sum, it) => sum + it.price, 0);
order.items.forEach(it => sendNotify(it));
saveToDB(order);
}
// 好:拆成三个单一职责函数
function calcTotal(items) { return items.reduce((sum, it) => sum + it.price, 0); }
function notifyItems(items) { items.forEach(it => sendNotify(it)); }
function processOrder(order) {
order.total = calcTotal(order.items);
notifyItems(order.items);
saveToDB(order);
}2. 参数数量控制
参数越多,调用方越容易传错,函数越难复用。经验法则:参数不超过 3 个,超过就用对象收拢(见"用对象替代参数")。
3. 尽早返回(卫语句)
把异常/边界情况提前 return,让主流程留在函数末尾,减少嵌套层级。
// 差:三层嵌套,主流程被埋住
function pay(user, order) {
if (user) {
if (user.isActive) {
if (order) {
return charge(user, order);
}
return "no order";
}
return "inactive";
}
return "no user";
}
// 好:卫语句逐个拦截,主流程最后一行
function pay(user, order) {
if (!user) return "no user";
if (!user.isActive) return "inactive";
if (!order) return "no order";
return charge(user, order);
}4. 纯函数优先
纯函数:同样的输入永远得到同样的输出,且不产生副作用(不改外部变量、不写 DOM、不发起请求)。纯函数易测试、易缓存、易并行,业务函数内部尽量"先纯后脏"——计算部分写成纯函数,副作用集中在最外层。
// 不纯:隐式依赖全局状态,不可预测
let taxRate = 0.1;
function priceWithTax(price) { return price * (1 + taxRate); }
// 纯:依赖显式传入,可测试可缓存
function priceWithTax(price, taxRate) { return price * (1 + taxRate); }5. 命名与函数长度
理想函数体控制在 10~20 行内;超过 40 行必然承担了多个职责,应当拆分。小函数之间的跳转成本远低于大函数带来的阅读负担。
三、模块划分
1. 高内聚、低耦合
- 高内聚:一个模块内部的成员强相关,共同完成一个清晰的目标。
- 低耦合:模块之间只通过明确的接口通信,不互相知道实现细节。
// user.js —— 只关心用户相关能力
export function getCurrentUser() { /* ... */ }
export function hasRole(user, role) { /* ... */ }
// order.js —— 只关心订单相关能力,通过接口使用 user 模块
import { getCurrentUser } from "./user.js";
export function createOrder(items) {
const user = getCurrentUser();
// ...只使用 user 暴露的接口,不直接操作 user 内部数据
}2. 按业务还是按能力组织
| 组织方式 | 结构 | 适合场景 |
|---|---|---|
| 按能力(技术分层) | api/、components/、utils/ | 中小项目、团队分工以技术栈为主 |
| 按业务(领域分包) | user/、order/、payment/(各自含组件/请求/工具) | 中大型项目、业务边界清晰 |
| 混合 | 顶层按业务,业务内按能力 | 最常见的折中方案 |
模块之间通过明确的导入导出边界约束:只导出需要公开的 API,内部辅助函数留在模块内,防止外部绕过接口依赖实现细节。
四、常见坏味道
坏味道是"重构的信号"。下表列出 JavaScript 中最常见的五种:
| 坏味道 | 表现 | 危害 | 对应重构 |
|---|---|---|---|
| 重复代码 | 同一逻辑在多个位置复制粘贴 | 修改要改多处,极易漏改 | 提取函数 |
| 过长函数 | 一个函数动辄上百行 | 无法理解、无法测试 | 提取函数、拆分循环 |
| 过大类 | 类承担多种不相关职责 | 职责纠缠、难以复用 | 拆分类(按职责拆分) |
| 魔法数字 | 裸数字直接出现在代码中 | 不知道含义,改一处漏一处 | 提炼常量 |
| 过长参数列表 | 四五个及以上参数 | 调用易错、难扩展 | 用对象替代参数 |
// 魔法数字示例
// 差
if (order.total > 100 && order.items.length > 10) { giveDiscount(order); }
// 好:命名常量让数字有了语义
const FREE_SHIPPING_THRESHOLD = 100;
const BULK_ITEM_COUNT = 10;
if (order.total > FREE_SHIPPING_THRESHOLD && order.items.length > BULK_ITEM_COUNT) {
giveDiscount(order);
}五、核心重构手法
1. 提取函数(Extract Function)
把一段有独立含义的代码块抽成一个命名函数,是最高频的重构手法。
// 重构前
function renderOrder(order) {
const total = order.items.reduce((s, it) => s + it.price * it.count, 0);
document.querySelector("#total").textContent = `¥${total}`;
// ... 还有很多渲染逻辑
}
// 重构后:先提取计算,再提取渲染
function calcOrderTotal(order) {
return order.items.reduce((s, it) => s + it.price * it.count, 0);
}
function renderOrderTotal(order) {
document.querySelector("#total").textContent = `¥${calcOrderTotal(order)}`;
}
function renderOrder(order) {
renderOrderTotal(order);
// ...其他渲染逻辑
}2. 提炼变量(Extract Variable)
复杂表达式中的子表达式语义不清,先算出来并命名。
// 重构前:一行的魔法逻辑
if (user.orders.length > 0 && user.orders[user.orders.length - 1].status === "paid") {}
// 重构后:先提炼变量
const hasOrder = user.orders.length > 0;
const lastOrderPaid = hasOrder && user.orders[user.orders.length - 1].status === "paid";
if (lastOrderPaid) {}3. 合并条件(Consolidate Condition)
多个条件走向同一结果时,合并成一个表达式,配合卫语句使用。
// 重构前
if (age < 18) return null;
if (age > 65) return null;
if (isBanned) return null;
// 重构后:条件合并,语义集中
if (age < 18 || age > 65 || isBanned) return null;4. 拆分循环(Split Loop)
一个循环里做了多件事(既要过滤又要统计又要渲染),拆成多个循环让每件事独立、可复用。现代引擎对短循环的多次遍历开销很小,可读性收益大于微性能损失。
// 重构前:一个循环三个职责
let total = 0;
const adults = [];
prices.forEach((p, i) => {
total += p;
if (users[i].age >= 18) adults.push(users[i]);
});
// 重构后:各自独立
const total = prices.reduce((s, p) => s + p, 0);
const adults = users.filter(u => u.age >= 18);5. 用对象替代参数(Replace Parameter with Object)
参数过多时,把相关参数收拢进一个对象,调用方更清晰,扩展也不破坏签名。
// 重构前:5 个位置参数
function createUser(name, age, email, isAdmin, avatar) {}
createUser("小明", 18, "x@m.com", false, "/a.png");
// 重构后:一个对象参数,可省略可扩展
function createUser({ name, age, email, isAdmin = false, avatar = "/default.png" }) {}
createUser({ name: "小明", age: 18, email: "x@m.com" });六、条件与循环重构
1. 卫语句消除嵌套
前面已介绍,核心是把分支从"主流程的分支"变成"提前退出的守卫",让主流程保持线性。
2. 策略模式替代多分支
当 if/else 或 switch 分支很多、且每个分支是一段独立策略时,用**对象映射(策略表)**替代:
// 重构前:新增一种支付方式就要改这个函数
function pay(type, amount) {
if (type === "alipay") return alipayPay(amount);
else if (type === "wechat") return wechatPay(amount);
else if (type === "card") return cardPay(amount);
else throw new Error("unsupported");
}
// 重构后:策略表,新增方式只加一行配置
const PAY_STRATEGIES = {
alipay: alipayPay,
wechat: wechatPay,
card: cardPay,
};
function pay(type, amount) {
const strategy = PAY_STRATEGIES[type];
if (!strategy) throw new Error(`unsupported: ${type}`);
return strategy(amount);
}| 对比维度 | 多分支 if/else | 策略表 |
|---|---|---|
| 新增分支 | 修改原函数,有漏改风险 | 只添加映射项 |
| 可读性 | 分支越多越难读 | 一目了然 |
| 复用 | 逻辑混在一起 | 每个策略独立可测 |
3. 循环内分支用 filter/map 先清洗
先过滤、后处理,比循环内 if 更符合声明式风格,这也是上一节"拆分循环"的自然延伸。
七、异步代码重构:回调 → Promise → async/await
异步代码的重构有一条清晰的演进主线,每一次演进都在解决前一阶段的可读性/错误处理问题。
1. 回调地狱
// 回调嵌套:难以阅读、错误处理分散
getUser(id, (err, user) => {
if (err) return handleErr(err);
getOrders(user.id, (err, orders) => {
if (err) return handleErr(err);
getDetails(orders[0].id, (err, detail) => {
if (err) return handleErr(err);
render(detail);
});
});
});2. Promise 化
// Promise 链:拍平嵌套,错误统一 catch
getUser(id)
.then(user => getOrders(user.id))
.then(orders => getDetails(orders[0].id))
.then(render)
.catch(handleErr);将回调式 API 包装为 Promise 的标准做法(promisify):
function getJson(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(res => res.json())
.then(resolve)
.catch(reject);
});
}3. async/await
// async/await:异步代码长得像同步代码,配合 try/catch 精确控制错误范围
async function renderOrderPage(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
const detail = await getDetails(orders[0].id);
render(detail);
} catch (err) {
handleErr(err);
}
}4. 需要并行的任务用 Promise.all
注意 await 串行会拖慢速度:互相没有依赖的请求应并行发起。
// 差:两个独立请求串行等待
const user = await fetchUser(id);
const banner = await fetchBanner();
// 好:并行发起,总耗时取最大值
const [user, banner] = await Promise.all([fetchUser(id), fetchBanner()]);| 演进阶段 | 嵌套深度 | 错误处理 | 流程控制 | 适用场景 |
|---|---|---|---|---|
| 回调 | 深 | 每个回调各管各的 | 困难 | 老代码、事件类 API |
| Promise | 浅 | 统一 catch | Promise.all/race | 工具函数、链式操作 |
| async/await | 同步化 | try/catch | 串行并行直观 | 业务代码首选 |
重构异步代码的顺序建议:先把回调改写为 Promise(用 promisify 或手写包装),再逐层把 .then 链改写为 async/await,最后检查是否有可并行的独立请求改用 Promise.all。
小结
规范与重构没有终点,但每轮投入都集中在同一件事上:让代码的意图显式化。命名让意图可见,函数拆小让意图聚焦,模块划分让意图隔离,坏味道识别让问题提前暴露,重构手法让修正低成本。形成"写代码时守规范、评审时找坏味道、定期小步重构"的习惯后,维护成本会持续下降,这正是代码质量管理的核心循环。