← 返回 Skill 列表
extension
分类: 开发与工程API Key 暂未确认

web-utilities-native-js

现代原生JavaScript(ES2022-ES2025)实用模式 - 数组方法、对象操作、集合运算、深度克隆以及可以替代lodash的函数工具

person作者: jakexiaohubgithub

Native JavaScript Utility Patterns

Quick Guide: Most utility-library calls now have a native equivalent with no bundle cost. The four that change how code is written are structuredClone for deep copies, the ES2023 immutable array methods (toSorted, toReversed, toSpliced, with), Object.groupBy / Map.groupBy, and the ES2025 Set operations. The two facts that catch people out: a structured clone drops prototypes and throws on functions, and the immutable array methods copy one level deep.

Detailed Resources:

  • examples/core.md — cloning, immutable updates, negative indexing, searching from the end
  • examples/arrays.md — deduplication, set operations, grouping, chunking, sorting, compacting
  • examples/objects.md — typed pick/omit/mapValues, deep merge, lookup maps, freezing
  • reference.md — lodash-to-native table, function utilities (debounce, throttle, memoize), availability

<critical_requirements>

Before writing utility code

Reach for the native method before adding a dependency. The lodash-to-native table in reference.md settles most of it in one lookup, and the native call ships nothing.

Deep-copy with structuredClone. A JSON round trip turns a Date into a string and a Map or Set into {}, and it does so silently — the failure surfaces later, at the first .getTime().

Use the to* array methods where the array is shared. sort, reverse and splice rewrite the array in place, so a function that "returns a sorted copy" with sort has changed its caller's data.

Convert to a Set before repeated membership checks. includes inside a filter is O(n²), and the difference shows up at a few thousand elements.

</critical_requirements>


Auto-detection: 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 alternative, native JavaScript utilities

Applies to:

  • Array work — deduplicating, grouping, chunking, flattening, sorting immutably, searching from the end
  • Object work — picking, omitting, merging, key/value transformation, lookup maps
  • Set algebra — union, intersection, difference, subset tests
  • Deep copying without a dependency
  • Hand-rolled function utilities — debounce, throttle, memoize, once

Handled elsewhere:

  • Date arithmetic, parsing and formatting — Date alone does not cover it
  • Reactive updates — these methods produce new references, and what consumes a new reference is the concern of whatever owns the state
  • Schema validation and runtime parsing — these utilities reshape data that is already trusted

<decision_framework>

Native or a dependency

Three cases, in order:

  • A native method exists — use it. The table in reference.md is the lookup.
  • No native method, but the implementation is ten lines — write it. chunk, pick, omit, compact, deepMerge, debounce, throttle, memoize and once are all in this skill, written out and typed.
  • The edge cases are the feature — take the dependency. A debounce with cancel, flush and leading/trailing edges; a deep merge with per-key array strategies; lazy evaluation over a dataset large enough that intermediate arrays matter. Reimplementing these badly costs more than the bytes.

</decision_framework>


<patterns>

Core patterns

Pattern 1: structuredClone for deep copies

Preserves Date, Map, Set, RegExp, typed arrays and circular references — everything a JSON round trip destroys.

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

Full code: examples/core.md


Pattern 2: Immutable array methods (ES2023)

Four to* methods return a new array where the classic method rewrote the old one.

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

The copy is shallow: the new array holds the same element objects, which is why the update above spreads before writing.

Full code: examples/core.md


Pattern 3: Object.groupBy and 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 coerces the key to a string and returns a null-prototype object, so reach for Object.hasOwn(byStatus, key) rather than byStatus.hasOwnProperty(key).

Full code: examples/arrays.md


Pattern 4: Set operations (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);

Each returns a Set, so spread when an array is wanted. The argument is any set-like with size, has and keys — a Map qualifies, a plain array does not.

Full code: examples/arrays.md


Pattern 5: Searching and indexing from the end

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

Full code: examples/core.md


Pattern 6: Object.fromEntries as the transformation pivot

entries → map/filter → fromEntries covers pick, omit, invert, mapValues, mapKeys and array-to-lookup in one shape, so those helpers are four lines each rather than a dependency.

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),
);

Full code: examples/objects.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • structuredClone on anything holding a function, a DOM node or a Proxy throws DataCloneError — clone the data separately from the behaviour.
  • JSON.parse(JSON.stringify(x)) does not throw; it corrupts. A Date becomes a string, a Map or Set becomes {}, undefined members vanish, and the first method call on the result is where it surfaces.
  • Object.groupBy returns a null-prototype object, so result.hasOwnProperty(...) throws. Use Object.hasOwn.
  • Set methods reject a plain array — they need something with size, has and keys.

Surprising behaviour:

  • structuredClone discards the prototype. A class instance clones to a plain object with the same fields and none of the methods.
  • toSorted() with no comparator sorts by string, so [10, 9, 1].toSorted() is [1, 10, 9] — exactly as sort() always did.
  • The to* methods copy one level. Editing an element of the new array edits the same object in the old one.
  • arr.at(-1) types as T | undefined whatever the compiler options, so the empty case has to be handled. arr[arr.length - 1] types as T unless noUncheckedIndexedAccess is on — that is the form that lets an undefined flow on unchecked.
  • structuredClone on an Error succeeds and keeps the standard fields, but a code, a status or anything else attached to it is not in the copy.
  • filter(Boolean) drops 0, "" and false along with the nullish values, and TypeScript does not narrow the result — filter((x): x is T => x != null) does both jobs correctly.
  • Object.freeze is shallow, and outside strict mode a write to a frozen object fails silently rather than throwing.
  • Object.keys returns integer-like keys first in ascending numeric order, whatever the insertion order — only string keys keep insertion order.

</red_flags>