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 里只保留最小决策。
命名动词+名词calculateDiscountresolveUserRole,避免 handledo 泛名。

9. 小结

  • 策略 / 表驱动减轻分支膨胀;工厂集中创建;模块边界事件降低耦合;**装饰(高阶函数)**叠加横切能力。
  • 性能优先在热路径上减少重复计算、选对数据结构、控制事件频率;再考虑微优化。
  • 实际项目里,先让结构正确、测试可覆盖,再针对 Profile 结果做针对性优化,往往比过早纠结一句语法更高效。
Last Updated 4/10/2026, 3:36:26 AM