计数器代码实现:添加、减少与重置功能

计数器代码实现:添加、减少与重置功能

计数器代码实现:添加、减少与重置功能

计数器是常见的交互组件:购物车里调整商品数量、表单里选择人数、设置页面里修改重试次数,都会用到“添加、减少、重置”。功能看起来简单,但实现时仍要明确几件事:计数器是否允许负数?每次变化多少?达到上下限后怎么办?重置回初始值,还是回到零?当数字来自用户输入或服务端时,又该如何校验?

本文先实现一个可直接在浏览器运行的计数器,再说明参数边界、常见错误、无障碍处理和服务端并发问题。示例采用整数计数;如果业务允许小数,需要另外设计精度与格式化规则。

一、计数器适合用在哪里

计数器适合表达一个可增减的数值状态,典型场景包括:

  • 数量选择:购物车商品数量、餐饮订单份数。通常有下限,库存等业务规则可能形成上限。
  • 配置选项:重试次数、并发任务数、分页大小。通常需要限制在合理范围内。
  • 简单交互状态:投票次数、步骤编号或本地演示中的计数值。
  • 服务端统计:访问量、库存、账户余额等。这些场景涉及并发与数据一致性,不能只靠浏览器里的加减按钮保证正确。

在动手编码前,应先确定计数规则。本文的示例会明确配置初始值、最小值、最大值和步长;到达边界时保持在边界,不继续变化。重置则回到配置的初始值,而不是固定归零。

二、可直接运行的浏览器示例

将下面的代码保存为 counter.html,再用现代浏览器打开。点击“添加”“减少”或“重置”即可操作。示例把界面、配置校验和状态更新放在一个文件中,方便先看清完整流程。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>计数器示例</title>
  <style>
    body {
      font-family: sans-serif;
      max-width: 32rem;
      margin: 3rem auto;
      padding: 0 1rem;
    }
    .value {
      font-size: 2rem;
      margin: 1rem 0;
    }
    button {
      margin-right: 0.5rem;
      padding: 0.5rem 0.8rem;
    }
    button:disabled {
      cursor: not-allowed;
      opacity: 0.55;
    }
  </style>
</head>
<body>
  <main>
    <h1>计数器</h1>
    <p>范围:0 到 10;每次变化 1。</p>
    <p class="value" id="value" aria-live="polite">3</p>
    <button id="decrease" type="button">减少</button>
    <button id="increase" type="button">添加</button>
    <button id="reset" type="button">重置</button>
  </main>

  <script>
    "use strict";

    const config = {
      initial: 3,
      min: 0,
      max: 10,
      step: 1
    };

    function isSafeInteger(value) {
      return Number.isSafeInteger(value);
    }

    function validateConfig(options) {
      const { initial, min, max, step } = options;

      if (![initial, min, max, step].every(isSafeInteger)) {
        throw new TypeError("initial、min、max 和 step 必须是安全整数");
      }
      if (min > max) {
        throw new RangeError("min 不能大于 max");
      }
      if (step <= 0) {
        throw new RangeError("step 必须是大于 0 的整数");
      }
      if (initial < min || initial > max) {
        throw new RangeError("initial 必须位于 min 和 max 之间");
      }
    }

    validateConfig(config);

    let count = config.initial;
    const valueElement = document.querySelector("#value");
    const decreaseButton = document.querySelector("#decrease");
    const increaseButton = document.querySelector("#increase");
    const resetButton = document.querySelector("#reset");

    function render() {
      valueElement.textContent = String(count);
      decreaseButton.disabled = count <= config.min;
      increaseButton.disabled = count >= config.max;
      resetButton.disabled = count === config.initial;
    }

    function changeBy(amount) {
      const candidate = count + amount;

      // 超出安全整数范围时,不展示已经失去整数精度的结果。
      if (!Number.isSafeInteger(candidate)) {
        count = amount > 0 ? config.max : config.min;
      } else {
        count = Math.min(config.max, Math.max(config.min, candidate));
      }

      render();
    }

    increaseButton.addEventListener("click", function () {
      changeBy(config.step);
    });

    decreaseButton.addEventListener("click", function () {
      changeBy(-config.step);
    });

    resetButton.addEventListener("click", function () {
      count = config.initial;
      render();
    });

    render();
  </script>
</body>
</html>

三、代码的关键部分

1. 把配置与当前状态分开

config 保存不会随点击变化的规则,count 保存当前值。两者分开后,重置逻辑就很直接:将 count 设回 config.initial。如果把初始值写死为零,遇到“从 3 开始计数”或“初始数量为 1”的需求,就容易出现重置行为不符合预期的问题。

配置在初始化时校验一次。示例要求初始值、上下限和步长都是安全整数,且步长大于零、最小值不大于最大值、初始值落在范围内。配置不合法时抛出异常,而不是悄悄换成另一个值。这样能更早暴露程序错误。

2. 用一个函数处理增减

添加和减少都调用 changeBy,区别只是传入正步长或负步长。这样边界处理集中在一处,不会出现“添加按钮会限制最大值,减少按钮却忘了限制最小值”这类逻辑分叉。

计算结果通过 Math.min 和 Math.max 限制在配置范围内。假设当前值为 9、最大值为 10、步长为 3,添加后会得到 10,而不是 12。对于某些业务,越界时也可以选择拒绝整次操作、显示提示或保持原值;应根据产品规则决定,不要把“钳制到边界”误认为唯一正确的行为。

3. 状态变化后统一更新界面

render() 同时更新数字和按钮状态。到达最小值后,减少按钮禁用;到达最大值后,添加按钮禁用;回到初始值后,重置按钮禁用。按钮禁用只是改善交互体验,不能替代状态更新函数中的边界检查,也不能当作服务端校验。

4. 检查 JavaScript 数字精度

JavaScript 的 Number 使用双精度浮点数表示数字。Number.MAX_SAFE_INTEGER 是 9007199254740991,超过安全整数范围后,并非每个整数都能准确表示。计数器如果可能接近这个范围,就不能只假设普通加法始终精确。

示例通过 Number.isSafeInteger 校验配置,并在计算结果不再是安全整数时将值设到对应边界。实际业务通常应该使用远小于这个极限的上下限。如果必须精确处理更大的整数,可以考虑用 BigInt 在客户端运算,或将权威计算放在数据库支持的整数类型中;但 BigInt 不能直接与普通 Number 混算,也不能直接用 JSON.stringify 序列化,接口格式需要专门约定。

四、如何复用为通用计数器函数

如果页面上有多个计数器,可以将规则封装为独立状态对象。下面的函数不操作 DOM,调用者负责读取返回值并更新界面,因而也更容易进行单元测试。

function createCounter(options) {
  const { initial, min, max, step } = options;

  const values = [initial, min, max, step];
  if (!values.every(Number.isSafeInteger)) {
    throw new TypeError("参数必须是安全整数");
  }
  if (min > max) {
    throw new RangeError("min 不能大于 max");
  }
  if (step <= 0) {
    throw new RangeError("step 必须大于 0");
  }
  if (initial < min || initial > max) {
    throw new RangeError("initial 必须处于边界范围内");
  }

  let value = initial;

  function update(delta) {
    const candidate = value + delta;

    if (!Number.isSafeInteger(candidate)) {
      value = delta > 0 ? max : min;
    } else {
      value = Math.max(min, Math.min(max, candidate));
    }

    return value;
  }

  return {
    getValue() {
      return value;
    },
    increase() {
      return update(step);
    },
    decrease() {
      return update(-step);
    },
    reset() {
      value = initial;
      return value;
    }
  };
}

const counter = createCounter({
  initial: 2,
  min: 0,
  max: 8,
  step: 2
});

console.log(counter.getValue()); // 2
console.log(counter.increase()); // 4
console.log(counter.decrease()); // 2
console.log(counter.reset());    // 2

这个实现把当前值放在闭包中,外部不能直接执行 counter.value = 100 修改它,只能通过公开的方法操作。不过,这只是 JavaScript 程序内部的封装,不是安全边界。若数值涉及库存、积分或金额,客户端代码可以被修改,不能据此认定操作可信。

五、参数限制该如何理解

步长。示例规定步长必须是正安全整数,因为“减少”会自动使用它的相反数。如果业务允许负步长,应先统一参数语义,避免传入负步长后添加按钮实际上执行减少。步长为零则没有变化,通常意味着配置错误。

上下限。需要确认上下限是包含式还是排除式。本文采用包含式,即 min 和 max 都是允许值。比如范围 0 到 10,0 和 10 都可以出现。若业务规则是“只能小于 10”,就应将规则明确实现为排除式,不能只靠文字描述。

初始值。示例要求初始值在范围内,并在配置错误时拒绝启动。另一种常见设计是自动把初始值钳制到范围内,例如把 12 调整为最大值 10。两种方式都可用,但自动修正可能掩盖配置错误;对于后台配置或开发期代码,显式报错通常更容易排查。

用户输入。如果改为文本框输入,不能只用 parseInt 后就当作输入合法。比如 parseInt("12abc", 10) 会得到 12,可能把格式错误伪装成有效数字。应先检查输入格式,再转换并验证有限性、整数性和业务范围。对于表单,可结合浏览器的 input 校验,但服务端仍需重新验证。

限制并非通用 API 保证。示例中的安全整数限制是 JavaScript Number 表示整数精度的边界,并不是对所有语言、数据库或业务场景都适用的统一上限。实际最大数量还可能受数据库字段类型、接口契约、业务规则和资源限制影响,必须依据项目真实约束确定。

六、常见错误与处理方式

  • 重置固定写成零。当初始值不是零时,重置应回到约定的初始状态。把初始值保存起来,比在多个事件处理器里重复写常量可靠。
  • 只在按钮上做禁用判断。按钮状态可能因代码调用、界面更新遗漏或其他入口而不同。边界判断应放在修改状态的函数里,按钮禁用只负责提示用户当前不可操作。
  • 添加和减少分别复制大量逻辑。重复实现容易出现一侧漏掉边界、另一侧忘记步长的问题。可以共用一个接收变化量的更新函数。
  • 把字符串直接拿来相加。从输入框读取的值通常是字符串,"2" + 1 会得到 "21"。转换前应验证输入,再明确转成数字;不要依赖隐式类型转换。
  • 只做前端校验。用户可以修改浏览器状态、绕过按钮或直接请求接口。前端校验帮助用户及时发现问题,不能作为可信数据的唯一校验。
  • 没有说明越界行为。“超过上限怎么办”可能意味着钳制、拒绝或报错,不同选择会带来不同体验。应在接口和产品行为中明确约定。
  • 忽略键盘和屏幕阅读器。用原生 button 元素可获得键盘操作支持;aria-live="polite" 可让辅助技术在适当时机获知数字更新。不要为了样式改成无法用键盘操作的普通文本元素。
  • 把展示值当成权威值。页面上的数字只是客户端显示状态。购物车数量、库存和账户余额等权威数据应由服务端验证和保存。

七、服务端计数器与并发更新

本地交互计数器通常由单个页面状态维护,直接加一即可。但若多个请求会同时更新同一个服务端计数,不能简单地先读取再写回。例如两个请求都读到库存为 5,各自计算减少 1,再分别写入 4,最终只减少了一次。更严重的情况还可能导致数据低于业务下限。

这类问题通常需要在数据库中使用原子更新、事务或适当的并发控制。以支持条件更新的 SQL 数据库为例,可以把边界条件放进同一条更新语句:

UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = ?
  AND quantity > 0;

服务端还需要检查受影响的行数,区分“成功减少”和“库存不足或记录不存在”。具体语法、锁行为和事务隔离取决于数据库产品及表结构,不能仅凭这段 SQL 推断所有环境下的并发语义。对累加型统计,也应结合数据库支持选择原子递增等操作,并考虑重试、去重和审计需求。

涉及金额时,不要直接用普通浮点数反复加减,例如用 0.1 作为十进制金额进行计算,可能出现二进制浮点误差。常见做法是按最小货币单位用整数保存,或采用明确支持定点数的数据库类型;具体方案还要符合币种小数位与业务规则。

八、测试清单

实现计数器后,可以至少检查以下行为:

  • 从初始值点击添加和减少,结果是否符合配置步长。
  • 到达最小值后继续减少,是否按预期保持、拒绝或提示。
  • 到达最大值后继续添加,是否按预期处理。
  • 步长大于 1 时,越过边界的结果是否符合约定。
  • 重置是否回到初始值,初始值不为零时是否正确。
  • 最小值大于最大值、步长为零、初始值越界或小数配置时,是否明确报错。
  • 数字接近安全整数边界时,是否避免显示失真的整数。
  • 使用键盘操作时,按钮能否获得焦点并响应回车或空格;数值变化能否被辅助技术感知。
  • 若数据由服务端维护,是否测试并发请求、重复提交和越界请求。

结语

一个可靠的计数器不只是给数字加一或减一,还需要定义初始值、步长、上下限、越界行为和重置语义。对于浏览器中的普通交互,可以把状态更新集中在一个函数里,统一做范围与精度检查;对于服务端统计、库存或金额等关键数据,则应由服务端和数据库执行校验与原子更新。先明确规则,再实现界面,能减少边界错误,也更容易把组件安全地复用到不同场景。

© 版权声明

相关文章