JavaScript 代码优化:设计模式与「更好写」的几种套路
JavaScript 代码优化:设计模式与「更好写」的几种套路
优化不是一上来就微优化,而是先分清目标:可读性(别人能改)、可维护性(需求变了少改漏)、运行性能(耗时与内存)。下面用常见设计思路(不必拘泥于类图里的名字)配合 Before / After 示例,说明怎么写通常更稳。
1. 先建立判断标准(避免无效优化)
| 维度 | 常见做法 |
|---|---|
| 可读性 | 意图清晰的命名、短函数、少嵌套、数据与逻辑分离。 |
| 结构 | 单一职责;变化多的分支用表驱动 / 策略代替超长 if-else。 |
| 性能 | 先量再改(Profiler);关注热路径上的重复计算、多余遍历、大对象拷贝。 |
下面例子均偏「日常业务代码」尺度,便于直接迁移。
2. 策略模式(Strategy):用映射表代替分支堆叠
问题:折扣、权限、状态机一类逻辑,用一长串 if-else / switch,每加一种就改同一块,易漏测、难读。
思路:把「条件 → 行为」抽成策略表(对象或 Map),主流程只负责查找并执行。
// Before:每多一种类型就要改函数内部
function priceOf(type, base) {
if (type === "vip") return base * 0.8;
if (type === "season") return base * 0.9;
if (type === "normal") return base;
return base;
}
// After:策略可独立扩展,主流程稳定
const discountStrategies = {
vip: (b) => b * 0.8,
season: (b) => b * 0.9,
normal: (b) => b,
};
function priceOf(type, base) {
const fn = discountStrategies[type] ?? discountStrategies.normal;
return fn(base);
}
收益:可读性上「一眼看到有哪些类型」;性能上避免深层分支预测劣势(差异通常不大,但结构更清晰)。复杂时可把每个策略换成独立模块再 import。
3. 工厂模式(Factory):统一创建,隐藏构造细节
问题:调用方到处 new 具体类或重复拼装配置,构造参数一变多处跟着改。
思路:用工厂函数集中创建,对外只暴露稳定接口(或只暴露产品类型枚举)。
// After:创建细节集中,便于换实现、打日志、接缓存
function createHttpClient(kind, options) {
const adapters = {
fetch: () => new FetchAdapter(options),
xhr: () => new XhrAdapter(options),
};
const ctor = adapters[kind];
if (!ctor) throw new Error(`unknown client: ${kind}`);
return ctor();
}
收益:可读性上调用点更短;维护时改工厂一处即可。性能上可在此处做单例复用(见下)而不污染业务代码。
4. 模块模式 / 显式边界:减少全局与隐式状态
问题:全局变量、let cache 散落在多个文件,难以推理与测试。
思路:用闭包或 ES Module 把「对外 API」与「内部状态」分开;只导出函数,不导出可变对象引用(或导出只读接口)。
// 模块内私有,外部只看到 createCounter
export function createCounter() {
let n = 0;
return {
inc: () => ++n,
value: () => n,
};
}
收益:可读性上边界清楚;性能上避免无关代码访问到内部字段,也便于以后替换实现(如持久化计数)。
5. 观察者 / 发布订阅(简化版):解耦「谁触发」与「谁响应」
问题:A 里直接调用 B、C、D,需求变成「再通知 E」时,A 要改个不停。
思路:事件中心(小型 EventEmitter)或框架内置事件;业务只订阅关心的事件。
// 极简发布订阅(生产环境可用成熟库)
function createBus() {
const all = new Map();
return {
on(event, fn) {
if (!all.has(event)) all.set(event, new Set());
all.get(event).add(fn);
return () => all.get(event)?.delete(fn);
},
emit(event, payload) {
all.get(event)?.forEach((fn) => fn(payload));
},
};
}
收益:扩展监听方不改核心流程;注意避免循环触发与内存泄漏(取消订阅)。
6. 装饰思路(组合优于继承):横向叠加能力
问题:用深层继承堆「日志 → 鉴权 → 缓存」,类爆炸。
思路:高阶函数包装异步或同步函数,一层一层装饰(类似中间件)。
function withTiming(fn) {
return async (...args) => {
const t0 = performance.now();
try {
return await fn(...args);
} finally {
console.log(`${fn.name || "fn"}: ${(performance.now() - t0).toFixed(1)}ms`);
}
};
}
const fetchUser = withTiming(async (id) => {
/* ... */
});
收益:可读性上能力显式分层;性能分析时可临时加 withTiming 而不改原函数体。
7. 性能侧:几个高杠杆习惯(与设计模式可叠加)
7.1 少做重复工作:记忆化(Memoization)
纯函数且输入集不大时,缓存结果避免重复计算。
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const v = fn(arg);
cache.set(arg, v);
return v;
};
}
适合:递归拆分、配置解析、昂贵字符串处理等;不适合:依赖实时时间、随机数、大参数对象作 key(内存暴涨)。
7.2 集合与查找:结构决定复杂度
- 频繁
includes/ 查找:大数组可换Set(平均 O(1) 级别判断存在性)。 - 频繁按键取值:用
Map或普通对象,避免在数组里线性搜。
7.3 延迟与批处理
用户输入、滚动、resize 等高频事件:防抖(debounce)、节流(throttle),减少无效计算与 DOM 操作(与设计模式无关,但对「体感性能」极重要)。
7.4 避免在热循环里创建大对象 / 闭包
循环内 new 大数组、反复创建函数,会增加 GC 压力;可提到循环外或复用缓冲区(视场景)。
8. 可读性侧:与模式配套的小习惯
| 习惯 | 说明 |
|---|---|
| 早返回 | 先处理异常/边界,主路径 留在函数后半段,缩进更浅。 |
| 纯函数优先 | 相同入参相同出参,便于测、便于缓存。 |
| 数据与逻辑分离 | 配置、文案、策略表是数据;if 里只保留最小决策。 |
| 命名动词+名词 | calculateDiscount、resolveUserRole,避免 handle、do 泛名。 |
9. 小结
- 策略 / 表驱动减轻分支膨胀;工厂集中创建;模块边界与事件降低耦合;**装饰(高阶函数)**叠加横切能力。
- 性能优先在热路径上减少重复计算、选对数据结构、控制事件频率;再考虑微优化。
- 实际项目里,先让结构正确、测试可覆盖,再针对 Profile 结果做针对性优化,往往比过早纠结一句语法更高效。
