正确做法是用闭包封装计数器和步长:工厂函数接收start和step参数,内部let id = start - step,返回() => (id += step);单例导出实例而非工厂函数,确保全局唯一序列。

用闭包实现带自增步长的原始 ID 生成器,核心是把计数器和步长都封在函数作用域内,只暴露一个调用即返回下一个 ID 的函数。这样既避免污染全局命名空间,又支持灵活控制每次递增的数值(比如每次+2、+10 或 -1),同时天然隔离多个生成器实例。
步长参数必须封装进闭包内部
步长不能靠外部变量传入或修改,否则就破坏了“不可篡改”前提。正确做法是在工厂函数中接收 step 参数,并用 let 声明私有计数器与步长,确保它们仅被返回的内部函数访问:
-
不要写成
let step = 1; return () => id += step;后再让别人改step = 5—— 这等于开了后门 -
应该把 step 作为形参固定在闭包环境里:
return (step = 1) => { let id = 0; return () => (id += step); }() - 更推荐显式初始化:
function createIdGenerator(step = 1) { let id = 0; return () => { id += step; return id; }; }
单例导出可彻底规避命名冲突
如果每个模块都自己调用 createIdGenerator(2),就会产生多个独立序列,ID 必然重复。真正防冲突的方式是:整个项目只初始化一次,导出已执行后的函数实例,而非工厂函数本身:
- 在
id.js中写:export const nextId = (() => { let id = 0; return () => ++id; })(); - 其他文件统一
import { nextId } from './id.js';,所有调用共享同一闭包状态 - 若需不同步长,可导出多个预设实例:
export const nextEvenId = createIdGenerator(2); export const nextBigId = createIdGenerator(100);
支持初始值 + 步长组合更实用
很多场景需要从非零起点开始(如 API 分页偏移、测试数据编号),只需扩展工厂函数,接受起始值与步长两个参数:
function createIdGenerator(start = 0, step = 1) { let id = start - step; return () => (id += step); }- 调用示例:
const userSeq = createIdGenerator(1001, 1); // 1001, 1002, 1003… const batchId = createIdGenerator(0, 10); // 0, 10, 20…- 注意:start 是第一个返回值,不是初始内部值,所以内部先减 step 再加,保证首调即得 start
不返回对象,只返回函数是最简最稳方案
任何带属性的对象(如 { next: () => ++id, reset() {} })都会引入可写字段风险。只要返回的是纯函数,JS 就没有语法层面的途径去篡改其闭包内的变量:
- ✅ 安全:
const gen = createIdGenerator(5); gen(); // 5 → 10 → 15… - ❌ 危险:
const gen = { id: 0, next() { return this.id += 5; } }; gen.id = 999; // 直接破坏 - 即使使用 getter,只要底层变量名暴露(如
get value() { return _id; }),仍可能被绕过;纯函数无字段可写,无名可查










