关于 web-utilities-native-js
面向 JavaScript 和 TypeScript 开发者的实用模式指南,说明如何用原生 API 替代常见工具库函数,并提醒克隆与不可变更新的边界。
主要功能
- 01原生 API 替代工具库函数
- 02深拷贝与结构化克隆
- 03不可变数组更新
- 04数组分组与 Set 运算
- 05对象转换与函数工具模式
适用场景
- 01减少实用工具依赖
- 02安全复制包含日期或集合的数据
- 03对共享数组执行不可变更新
- 04按键分组数据并执行集合运算
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-utilities-native-js安装后在 Claude Code 中输入/web-utilities-native-js确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/arrays.mdcore.mdobjects.md详细介绍
原生 JavaScript 工具模式
快速指南: 大多数工具库调用现在都有原生等效方法,且不会增加打包体积。 会改变代码编写方式的有四项:用于深拷贝的
structuredClone、ES2023 不可变数组方法(toSorted、toReversed、toSpliced、with)、Object.groupBy/Map.groupBy,以及 ES2025 Set 操作。两个容易让人意外的事实:结构化克隆会丢弃原型,遇到函数时会抛出异常;不可变数组方法只会进行一层浅拷贝。
详细资源:
- examples/core.md — 克隆、不可变更新、负索引、从末尾搜索
- examples/arrays.md — 去重、集合操作、分组、分块、排序、清理
- examples/objects.md — 类型化的 pick/omit/mapValues、深度合并、查找映射、冻结
- reference.md — lodash 到原生方法对照表、函数工具(debounce、throttle、memoize)、可用性
<critical_requirements>
编写工具代码之前
优先使用原生方法,再考虑添加依赖。 reference.md 中的 lodash 到原生方法对照表可以通过一次查找解决大多数问题,而且原生调用不会带来额外内容。
使用 structuredClone 进行深拷贝。 JSON 往返转换会将 Date 变成字符串,将 Map 或 Set 变成 {},而且不会报错——问题会在之后首次调用 .getTime() 时才暴露。
数组会被多个地方共用时,使用 to* 数组方法。 sort、reverse 和 splice 会原地改写数组,因此,使用 sort 来“返回排序后的副本”的函数实际上修改了调用方的数据。
反复检查成员关系之前,先转换为 Set。 在 filter 中使用 includes 会导致 O(n²) 的复杂度,元素达到几千个时就会显现差异。
</critical_requirements>
自动检测: structuredClone、Object.groupBy、Map.groupBy、toSorted、toReversed、toSpliced、 Array.prototype.with、findLast、findLastIndex、at(-1)、Object.hasOwn、Object.fromEntries、 Set.prototype.union、Set.prototype.intersection、Set.prototype.difference、symmetricDifference、 isSubsetOf、isDisjointFrom、lodash 替代方案、原生 JavaScript 工具
适用范围:
- 数组操作 — 去重、分组、分块、扁平化、不可变排序、从末尾搜索
- 对象操作 — 挑选、排除、合并、键/值转换、查找映射
- 集合代数 — 并集、交集、差集、子集测试
- 无需依赖即可进行深拷贝
- 手写函数工具 — debounce、throttle、memoize、once
其他内容:
- 日期运算、解析和格式化 — 单靠
Date无法满足需求 - 响应式更新 — 这些方法会生成新的引用,而如何处理新引用取决于负责管理状态的对象
- 模式验证和运行时解析 — 这些工具会重塑已可信的数据
<decision_framework>
原生方法还是依赖
按顺序考虑以下三种情况:
- 存在原生方法 — 使用它。reference.md 中的对照表可供查阅。
- 没有原生方法,但实现只需十行代码 — 自行编写。本技能中提供了
chunk、pick、omit、compact、deepMerge、debounce、throttle、memoize和once的完整类型化实现。 - 边缘情况本身就是功能的一部分 — 使用依赖。支持
cancel、flush和前沿/后沿触发的 debounce;支持按键采用不同数组策略的深度合并;对足够大的数据集进行惰性求值,使中间数组会造成影响。重新实现这些功能却处理不当,代价远高于它们占用的字节数。
</decision_framework>
核心模式
模式 1:使用 structuredClone 进行深拷贝
保留 Date、Map、Set、RegExp、类型化数组和循环引用——这些都是 JSON 往返转换会破坏的内容。
const snapshot = structuredClone(state);
// Preserved: Date, Map, Set, RegExp, ArrayBuffer, circular refs
// Throws on: functions, DOM nodes, Proxy, symbols
// Silently lost: the prototype — a class instance clones to a plain object
完整代码:examples/core.md
### 模式 2:不可变数组方法(ES2023)
四种 to* 方法会返回新数组,而经典方法会改写原数组。
items.toSorted((a, b) => a.price - b.price); // vs .sort()
items.toReversed(); // vs .reverse()
items.toSpliced(index, 1); // vs .splice()
items.with(index, { ...items[index], done: true }); // vs items[i] = x
复制是浅复制:新数组持有相同的元素对象,因此上面的更新会先展开再写入。
完整代码:examples/core.md
模式 3:Object.groupBy 和 Map.groupBy(ES2024)
const byStatus = Object.groupBy(orders, (order) => order.status);
// { pending: [...], shipped: [...] } — null-prototype, values are `T[] | undefined`
const byCustomer = Map.groupBy(orders, (order) => order.customer);
// Map keyed by the object itself — use this when the key is not a string
Object.groupBy 会将键强制转换为字符串,并返回一个无原型对象,因此应使用
Object.hasOwn(byStatus, key),而不是 byStatus.hasOwnProperty(key)。
完整代码:examples/arrays.md
模式 4:Set 操作(ES2025)
setA.union(setB);
setA.intersection(setB);
setA.difference(setB); // in A, not in B
setA.symmetricDifference(setB);
setA.isSubsetOf(setB);
setA.isSupersetOf(setB);
setA.isDisjointFrom(setB);
每种操作都会返回一个 Set,因此需要数组时请展开。参数可以是任何具备 size、
has 和 keys 的类 Set 对象——Map 符合要求,普通数组则不符合。
完整代码:examples/arrays.md
模式 5:从末尾搜索和索引
logs.findLast((log) => log.level === "error"); // ES2023, single pass
logs.findLastIndex((log) => log.level === "error");
items.at(-1); // ES2022, undefined when empty
"file.backup.txt".split(".").at(-1); // "txt" — strings too
完整代码:examples/core.md
模式 6:将 Object.fromEntries 作为转换枢纽
entries → map/filter → fromEntries 这一模式可涵盖 pick、omit、invert、mapValues、mapKeys 和数组转查找表,
因此这些辅助函数各只需四行代码,无须引入依赖。
const keyed = Object.fromEntries(items.map((item) => [item.id, item]));
const withTax = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 1.1]),
);
const passing = Object.fromEntries(
Object.entries(scores).filter(([, score]) => score >= 90),
);
完整代码:examples/objects.md
<red_flags>
注意事项
运行时会出错:
- 对包含函数、DOM 节点或 Proxy 的内容调用
structuredClone会抛出DataCloneError——请将数据与行为分开克隆。 JSON.parse(JSON.stringify(x))不会抛出异常,而是会破坏数据。Date会变成字符串,Map或Set会变成{},undefined成员会消失,而问题会在对结果进行首次方法调用时显现。Object.groupBy返回无原型对象,因此result.hasOwnProperty(...)会抛出异常。请使用Object.hasOwn。- Set 方法会拒绝普通数组——它们需要具备
size、has和keys的对象。
行为可能出乎意料:
structuredClone会丢弃原型。类实例会被克隆为字段相同但不包含任何方法的普通对象。- 不传比较器时,
toSorted()会按字符串排序,因此[10, 9, 1].toSorted()的结果是[1, 10, 9]——这与sort()一直以来的行为完全相同。 to*方法只会复制一层。修改新数组中的元素,也会修改旧数组中的同一个对象。- 无论编译器选项如何,
arr.at(-1)的类型都是T | undefined,因此必须处理空数组的情况。除非启用了noUncheckedIndexedAccess,否则arr[arr.length - 1]的类型是T——正是这种形式会在未进行检查时让undefined流入。 - 对
Error调用structuredClone会成功并保留标准字段,但附加在其上的code、status或其他内容不会出现在副本中。 filter(Boolean)会连同空值一起过滤掉0、""和false,而 TypeScript 不会收窄结果类型——filter((x): x is T => x != null)可以正确完成这两项工作。Object.freeze是浅冻结;在严格模式之外,对冻结对象的写入会静默失败,而不是抛出异常。Object.keys会先按升序返回类似整数的键,无论插入顺序如何——只有字符串键会保留插入顺序。
</red_flags>