array.prototype.with是es2023引入的不可变单索引更新方法,性能优于map和展开运算符,但需注意引用变化、边界处理及与tospliced的适用场景区分。

Array.prototype.with 是 ES2023 引入的原生方法,它能在不修改原数组的前提下,安全、简洁地替换指定索引处的元素——这正是状态管理中“不可变更新数组某一项”的理想工具。但它不是万能的,尤其在 React/Vue/Pinia 等框架的状态流中,直接用错位置或忽略边界条件,反而会触发不必要的重渲染或静默失败。
为什么 with 比 map 或展开运算符更快更安全?
常见做法是 [...arr.slice(0, i), newItem, ...arr.slice(i + 1)] 或 arr.map((x, idx) => idx === i ? newItem : x),但它们都涉及新建多个子数组或遍历全量元素。with 底层可被引擎优化为仅复制必要结构(如稀疏数组处理、内部存储切片),实测在大数组(>10k)中更新中间项时,性能提升约 30–50%;更重要的是语义明确:只改一个索引,无副作用。
-
with不会改变原数组,返回新数组,天然契合不可变范式 - 它不调用回调函数,避免
map中闭包捕获、this 绑定等隐性开销 - 对负索引支持良好(
arr.with(-1, 'last')等价于arr.with(arr.length - 1, 'last')),比手写slice更鲁棒 - 注意:它不支持 Symbol 键或非数字索引,仅接受整数索引(含负数)
with 在 React 状态更新中为何有时不触发重渲染?
根本原因是浅比较失效:如果新数组与旧数组引用相同(比如你误用了 arr.with(i, sameRefItem) 且其他项引用未变),React.memo 或 useMemo 就会跳过更新。这不是 with 的 bug,而是不可变更新的共性陷阱。
- 确保被替换的值本身也是新引用:若更新对象项,别传原对象,用
{...oldObj, key: newVal}或structuredClone - 避免在渲染函数内反复调用
with生成新数组(如放在useCallback外),否则每次 render 都创建新引用,抵消 memo 效果 - 在
useState更新中,推荐包裹一层函数式更新:setItems(prev => prev.with(i, newItem)),防止竞态 - 注意:V8 当前对
with返回数组的内部表示做了优化,但 React 仍依赖===判断,所以引用变化必须真实发生
和 toSpliced、with 混用时的典型误用场景
ES2023 同时引入了 toSpliced(用于插入/删除/替换多元素),很多人想“组合使用”,结果写出冗余逻辑。例如想更新第 i 项并插入新项到末尾,错误写法:arr.toSpliced(i, 1, newItem).with(-1, appended) —— 这样做了两次拷贝,且索引易错乱。
- 单点更新 → 无条件用
with - 增删+单点更新混合 → 优先用
toSpliced一步到位,例如arr.toSpliced(i, 1, newItem)本身就等价于with -
with不支持长度变更,越界索引(如i >= arr.length)会静默忽略(返回原数组),而toSpliced支持自动扩容 - TypeScript 用户注意:目前
@types/node和主流库类型定义已支持with,但若用老版lib(如es2022),需显式升级到es2023或添加// @ts-expect-error临时绕过
真正关键的不是“怎么写”,而是理解 with 的能力边界:它只解决“单索引精确覆盖”这一件事。一旦需求变成“根据条件找索引再更新”“批量更新多个索引”“更新后排序”,就得退回到 map 或 toSpliced,或者用 immer 这类工具抽象。别为了用新语法而硬套,状态更新的正确性永远比写法炫酷重要。










