首页 / 文章 / 六种用于避免代码混乱的JavaScript设计模式

六种用于避免代码混乱的JavaScript设计模式

介绍了六种实用的 JavaScript 模式——策略模式、工厂模式、观察者模式、适配器模式、组合模式和管道模式,它们能够用易于维护的结构取代混乱的代码。

2146 词

将混乱的脚本转化为可预测、易维护的系统

任何从事软件开发较长时间的人都会遇到这样的尴尬时刻:在项目发布数月后重新打开它时,已无法理清各部分之间的关联。

这种混乱状况很少是故意造成的。你先为界面添加一个快速切换功能,接着是数据获取代码,再处理一些边缘情况,然后加上加载指示器,最后还加入分析功能调用。几周之后,原本整洁的脚本就变成了一个包含800多行代码的庞然大物,充斥着嵌套回调、杂乱的全局变量以及脆弱的if/else结构。

这就是“意大利面代码”的本质:业务规则与展示逻辑相互交织得如此紧密,以至于稍改一处就会影响到其他两个部分。

构建可扩展的 JavaScript 并不意味着要为每个函数都添加繁重的企业级抽象层。关键在于分离关注点,并依赖少数可靠的模式。

以下是六种经过验证的设计与架构模式,它们能够整理杂乱的 JavaScript 代码,帮助你在代码库规模扩大时仍能保持可控性。

1. 策略模式:消除嵌套条件语句

问题所在

每当逻辑需要根据用户类别、支付方式或处理模式进行分支时,许多开发者的第一反应就是堆砌 if/else 语句或庞大的 switch 块。

// The Spaghetti Way
function calculateShipping(order) {
  if (order.type === 'standard') {
    return order.weight * 1.5;
  } else if (order.type === 'express') {
    return order.weight * 3.0 + 10;
  } else if (order.type === 'overnight') {
    return order.weight * 5.0 + 25;
  } else if (order.type === 'international') {
    return order.weight * 8.0 + 50;
  } else {
    throw new Error('Unknown shipping method');
  }
}

每当团队新增一种配送等级时,都不得不修改这个核心函数。此处的一个错误或故障就会导致所有订单类型的配送计算全部出错。

解决方案

策略模式将每种算法单独提取为独立的函数,并存储在共享的查找对象中。

// The Scalable Way
const shippingStrategies = {
  standard: (order) => order.weight * 1.5,
  express: (order) => order.weight * 3.0 + 10,
  overnight: (order) => order.weight * 5.0 + 25,
  international: (order) => order.weight * 8.0 + 50,
};

function calculateShipping(order) {
  const strategy = shippingStrategies[order.type];

  if (!strategy) {
    throw new Error(`Unsupported shipping method: ${order.type}`);
  }

  return strategy(order);
}

为何该方案能在大规模应用中保持稳定

  • 开闭原则:新增十几种配送选项只需在shippingStrategies中添加新条目,无需修改calculateShipping函数本身。
  • 更易于测试:每种策略函数都可以单独导出、性能分析及测试。

2. 模块模式与工厂模式:控制状态范围

问题所在

作用域不明确的全局变量以及可共享的易变对象容易引发难以排查的错误。一旦多个 UI 组件能够自由读取和修改同一状态,要确定是哪个组件破坏了该值就如同在搞侦探工作。

// The Spaghetti Way
let cart = [];
let total = 0;

function addItem(item) {
  cart.push(item);
  total += item.price;
}

function resetCart() {
  cart = [];
  total = 0;
}

页面上其他无关的脚本完全可以随意将cart设置为 null,或更新total的值,而无需同步修改cart

解决方案

工厂函数中使用闭包可以将状态保持为私有,仅暴露其他代码实际需要的操作,同时完全隐藏原始变量。

// The Scalable Way
function createCart() {
  // Private variables protected inside the closure
  let items = [];

  return {
    addItem(product) {
      if (!product || typeof product.price !== 'number') {
        throw new Error('Invalid product payload');
      }
      items.push({ ...product, id: crypto.randomUUID() });
    },
    removeItem(productId) {
      items = items.filter((item) => item.id !== productId);
    },
    getItems() {
      // Return a shallow copy so external mutations don't alter state
      return [...items];
    },
    getTotal() {
      return items.reduce((sum, item) => sum + item.price, 0);
    },
    clear() {
      items = [];
    }
  };
}

const userCart = createCart();
userCart.addItem({ name: 'Mechanical Keyboard', price: 120 });
console.log(userCart.getTotal()); // 120

为何此方法在大规模应用中依然有效

  • 无变量泄露:外部代码无法直接覆盖items——必须通过经过验证的公共方法来操作。
  • 安全的多实例机制:每次调用createCart()都会返回独立的状态,不存在一个实例破坏另一个实例的风险。

3. 观察者模式(发布/订阅):降低代码的紧密耦合度

问题所在

当顾客点击“下单”时,需要同时执行多项操作:清空购物车、显示确认信息、发送追踪像素,并通知后端。将所有这些逻辑塞进一个函数中,会导致其变得难以管理。

// The Spaghetti Way
async function handleCheckout(order) {
  await api.submitOrder(order);

  // UI logic mixed directly with tracking and data operations
  document.querySelector('#cart-count').textContent = '0';
  document.querySelector('#modal').classList.add('active');
  analytics.trackPurchase(order);
  notificationSystem.sendPush('Order confirmed');
}

如果跟踪脚本出现错误或DOM元素被重命名,整个结账流程就有失败的风险。

解决方案

// The Scalable Way
class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  subscribe(eventName, listener) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, new Set());
    }
    this.events.get(eventName).add(listener);

    // Return an easy unsubscribe function
    return () => this.events.get(eventName).delete(listener);
  }

  publish(eventName, data) {
    const listeners = this.events.get(eventName);
    if (listeners) {
      listeners.forEach((listener) => {
        try {
          listener(data);
        } catch (err) {
          console.error(`Error executing listener for ${eventName}:`, err);
        }
      });
    }
  }
}

const appBus = new EventEmitter();

// Feature modules register their own behavior
appBus.subscribe('order:placed', (order) => {
  analytics.trackPurchase(order);
});

appBus.subscribe('order:placed', () => {
  document.querySelector('#cart-count').textContent = '0';
});

// The emitter stays minimal and decoupled
async function handleCheckout(order) {
  await api.submitOrder(order);
  appBus.publish('order:placed', order);
}

为何该方案能在大规模环境中稳定运行

  • 各部分之间无依赖关系:结账函数并不知道有哪些组件正在使用它。您可以添加新的分析功能、邮件通知或界面效果,而无需修改handleCheckout函数。
  • 故障隔离:某个监听器出现故障不会影响触发该事件的函数。

4. 适配器模式:让代码免受不稳定依赖的影响

问题所在

外部服务、npm 包以及内部接口常常会在毫无预警的情况下更改其接口规范。如果有 15 个独立的组件用于获取用户数据,且每个组件都直接读取原始响应字段,那么只需将某个属性的名称从 user_id 更改为 id,就可能需要对整个代码库进行重构。

// The Spaghetti Way: scattered across multiple UI components
function renderProfile(rawApiResponse) {
  // Directly tied to backend-specific naming conventions
  const name = `${rawApiResponse.first_name} ${rawApiResponse.last_name}`;
  const address = rawApiResponse.shipping_address_line_1;
  const avatar = rawApiResponse.meta_info.profile_image_url;
}

解决方案

在外部数据源与应用程序的内部逻辑之间添加一个 适配器 层。在下游代码处理数据之前,先将外部传入的数据转换为稳定、可预测的结构。

// The Scalable Way
function userAdapter(externalUser) {
  return {
    id: externalUser.user_id || externalUser.id,
    fullName: `${externalUser.first_name || ''} ${externalUser.last_name || ''}`.trim(),
    address: externalUser.shipping_address_line_1 || externalUser.street || 'N/A',
    avatar: externalUser.meta_info?.profile_image_url || '/assets/default-avatar.png',
  };
}

// Your components only ever consume normalized models
async function getUserProfile(userId) {
  const response = await fetch(`/api/v1/users/${userId}`);
  const rawData = await response.json();
  return userAdapter(rawData);
}

为何此方法在大规模应用中依然有效

  • 仅需调整一处:如果后端在下周更改响应架构,只需修改一次userAdapter,而无需逐一修复四十个出问题的组件。
  • 更简单的测试替身:UI测试只需验证标准化后的数据结构,无需处理不断变化的外部格式。

5. 优先使用组合而非继承:像积木一样构建功能

问题所在

过深的类层次结构往往会被自身的复杂性所拖累。假设你从一个通用的User类开始,再分支出AdminUserModeratorUserGuestUser。一旦需要GuestModerator——即拥有部分管理权限但并非全部权限的用户——整个结构就会出现问题。

// The Spaghetti Way: Deep Inheritance
class BaseUser {
  login() { /* ... */ }
}

class Admin extends BaseUser {
  deleteContent() { /* ... */ }
  manageBilling() { /* ... */ }
}

// What happens when you need a "BillingAgent" who cannot delete content?

过长的继承链以不符合现实的方式将各种功能绑定在一起,导致子类最终继承了对其而言毫无意义的方法。

解决方案

应改用对象组合:根据需要将小型、可重用的行为函数(混入器)附加到对象上。围绕对象的实际功能来构建能力集,而非强制将其纳入僵化的“是……”层次结构中。

// The Scalable Way: Composable Behaviors
const canAuthenticate = (state) => ({
  login: () => console.log(`${state.email} logged in`),
  logout: () => console.log(`${state.email} logged out`),
});

const canModerateContent = () => ({
  deletePost: (postId) => console.log(`Post ${postId} deleted`),
  banUser: (userId) => console.log(`User ${userId} banned`),
});

const canManageBilling = () => ({
  processInvoice: (amount) => console.log(`Invoice processed: ${amount}`),
});

// Build specialized actors on demand
function createSupportStaff(email) {
  const state = { email };
  return {
    email,
    ...canAuthenticate(state),
    ...canModerateContent(),
  };
}

function createSuperAdmin(email) {
  const state = { email };
  return {
    email,
    ...canAuthenticate(state),
    ...canModerateContent(),
    ...canManageBilling(),
  };
}

const moderator = createSupportStaff('support@example.com');
moderator.login();
moderator.deletePost(404);
// moderator.processInvoice is undefined - zero privilege leakage

为何这种方式在大规模应用中依然有效

  • 无需管理层次结构:行为可以即时组合,无需提前规划类树。
  • 可在不同场景下使用:canAuthenticate 这样的功能在客户账户、内部员工账户或自动化机器人账户上都能同样良好地运行。
  • 6. 流水线模式:顺序异步步骤

    问题所在

    将多个异步转换步骤串联起来,往往会导致代码嵌套过深、难以理解,还会将彼此无关的功能混在一起。

    // The Spaghetti Way
    async function handleImageUpload(file) {
      if (file.size > 5000000) {
        throw new Error('Too large');
      }
      const compressed = await compressImage(file);
      const metadata = await extractExif(compressed);
      const tagged = await tagCategories(compressed, metadata);
      const uploadResult = await uploadToS3(tagged);
      return uploadResult;
    }
    

    通过三个步骤可以应对这个问题,但一旦开始加入日志记录、性能监控、重试机制和验证功能,整个结构就会变得难以理解。

    解决方案

    采用管道模式:将每一步转换视为一个功能单一的小函数,并将它们串联起来,从而使整个流程从开始到结束、从上到下或从左到右都能清晰呈现。

    // The Scalable Way
    const pipeAsync = (...functions) => (initialValue) =>
      functions.reduce(
        (currentPromise, currentFunction) => currentPromise.then(currentFunction),
        Promise.resolve(initialValue)
      );
    
    // Each step is an isolated, testable transformation
    const validateSize = async (file) => {
      if (file.size > 5 * 1024 * 1024) throw new Error('File exceeds 5MB limit');
      return file;
    };
    
    const compress = async (file) => compressImage(file);
    const attachWatermark = async (image) => applyWatermark(image);
    const upload = async (finalImage) => uploadToCloud(finalImage);
    
    // Create the pipeline
    const processUserImage = pipeAsync(
      validateSize,
      compress,
      attachWatermark,
      upload
    );
    
    // Usage
    processUserImage(rawFileInput)
      .then((res) => console.log('Upload complete:', res))
      .catch((err) => console.error('Pipeline failed:', err.message));
    

    为何该模式能在大规模应用中保持高效

    • 易于重新排序:添加、删除或调整步骤顺序——比如插入缩略图生成环节——几乎不需要花费太多精力。
    • 逐步调试:可以在管道的任意位置插入简单的日志记录函数,以便查看输入和输出内容。

    摆脱混乱的代码

    代码变得杂乱并非因为编写者能力不足,而是因为系统在时间压力下不断扩展,开发者往往会选择最能快速解决当前问题的代码行。

    实现整洁架构的真正关键并非强行使用那些功能繁重的企业级框架,而是根据实际需求在合适的位置运用简洁且经过深思的结构:

    1. 让模式匹配实际问题:当条件判断变得难以控制时使用策略模式,当模块之间开始陷入循环调用时引入发布/订阅机制,而当第三方接口可能破坏用户界面稳定性时则使用适配器模式
    2. 优先选择简单方案而非复杂设计:不必一开始就使用所有模式。等某段逻辑出现两次重复后再考虑在第三次时进行抽象处理。
    3. 保持函数纯粹且接口定义清晰:可预测的输入与输出能大幅降低日后代码重构的难度。

    优质代码并非一次性就能写得完美无缺——它应该是即便在六个月后依然易于修改的代码。

    相关阅读

  • Cursor、Claude Code 与 Codex:为 JS 选择 AI 编程工具 —— 本文对比了 Cursor、Claude Code 和 Codex 如何适配不同的 JavaScript 工作流程,从基于编辑器的编程到自主智能体任务。
  • 十种常见的 JavaScript 模式及其各自的利弊 —— 守护子句、查找映射表、结果对象、AbortController、allSettled、工厂函数与小型缓存:了解每种模式何时能发挥优势,何时会适得其反。