core-js 中的 Change `Array` by copy 提案实现:toReversed、toSorted、toSpliced、with 的规范解析与实战使用
2026/9/12 1:48:39 网站建设 项目流程

core-js 中的 ChangeArrayby copy 提案实现:toReversed、toSorted、toSpliced、with 的规范解析与实战使用

【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js

本指南围绕 core-js 对 TC39「ChangeArrayby copy」提案(已合入 ECMA-262 标准)的完整支持展开,覆盖 Array 与 %TypedArray% 两组复制式变更方法的规范签名、每个方法的源码级实现原理、按需引入的 Entry Points,以及配套单元测试所验证的边界行为。读完本文,你将能在项目中安全、精准地使用这四个返回新数组的方法,彻底告别slice().reverse()slice().sort()这类临时拷贝写法,并理解 core-js 是如何在旧环境中补齐这些现代语义的。

提案背景:为什么要"按副本变更"

传统的Array.prototype.reversesortsplice都会原地修改调用者数组。为了不破坏原数据,开发者只能先手动拷贝一份再操作:

const original = [3, 1, 2]; const reversed = original.slice().reverse(); // 需要临时 slice 一次 const sorted = original.slice().sort(); // 同上

「ChangeArrayby copy」提案的核心思路,是为每个原地方法提供一个以to开头的"复制式"对应物:返回一个新数组/新 TypedArray,原对象保持不变。在 core-js 仓库中,这一功能已从提案阶段进入标准实现:例如 es.array.to-reversed.js 的注释已直接指向 ECMA-262 规范章节https://tc39.es/ecma262/#sec-array.prototype.toreversed,说明这些方法在 core-js 中已作为es.*标准模块维护,并被 stage/4.js 引入(对应 Stage 4 已完成提案的自动启用入口)。

规范签名:Array 与 %TypedArray%

提案定义了 8 个方法(普通数组 4 个、类型化数组 3 个)。原文档给出的 TypeScript 风格签名如下,其中%TypedArray%泛指Int8ArrayUint8ArrayBigInt64Array等全部类型化数组子类:

class Array { toReversed(): Array<mixed>; toSpliced(start?: number, deleteCount?: number, ...items: Array<mixed>): Array<mixed>; toSorted(comparefn?: (a: any, b: any) => number): Array<mixed>; with(index: number, value: any): Array<mixed>; } class %TypedArray% { toReversed(): %TypedArray%; toSorted(comparefn?: (a: any, b: any) => number): %TypedArray%; with(index: number, value: any): %TypedArray%; }

注:原文档签名中with的参数写作index: includes,应为number类型的笔误;规范与 core-js 源码中该参数均为相对索引(支持负值),详见下文各方法解析。

三个方法的核心语义:

方法等价于(原地位操作)返回值
toReversed()reverse()反转后的新数组
toSorted(comparefn?)sort(comparefn?)排序后的新数组
toSpliced(start, deleteCount?, ...items)splice(start, deleteCount, ...items)增删元素后的新数组
with(index, value)arr[index] = value替换单个元素后的新数组

注意%TypedArray%只有toReversed/toSorted/with三个标准方法,没有标准的toSpliced。这一点在 core-js 的提案入口中体现得非常清楚(见下文 Entry Points 章节)。

Array 系列方法的源码级实现

toReversed:复制反转

es.array.to-reversed.js 的实现非常直观——先按length创建一个新数组,再逆序拷贝元素:

$({ target: 'Array', proto: true }, { toReversed: function toReversed() { var O = toIndexedObject(this); var len = lengthOfArrayLike(O); var A = new $Array(len); var k = 0; for (; k < len; k++) createProperty(A, k, O[len - k - 1]); return A; } }); addToUnscopables('toReversed');

关键细节:

  • toIndexedObject(this)先做索引化对象转换,因此任意类数组对象都能调用(测试 es.array.to-reversed.js 专门验证了带length的普通对象目标);
  • createProperty逐个定义元素,跳过稀疏空洞,与规范要求的 hole 语义一致;
  • 末尾addToUnscopables('toReversed')将方法名写入Array.prototype[Symbol.unscopables],防止with (array)环境下与旧式变量名冲突(测试第 61 行断言了这一点)。

单元测试还验证了其"不可变"与"非泛型"属性:assert.notSame(array.toReversed(), array, 'immutable')确保返回全新数组;即使目标对象被篡改constructor[Symbol.species]toReversed依然返回真正的Array实例(tests/unit-global/es.array.to-reversed.js)。

toSorted:先拷贝再排序

es.array.to-sorted.js 巧妙地复用了原生sort,避免重复实现排序算法:

var sort = uncurryThis(getBuiltInPrototypeMethod('Array', 'sort')); $({ target: 'Array', proto: true }, { toSorted: function toSorted(compareFn) { if (compareFn !== undefined) aCallable(compareFn); var O = toIndexedObject(this); var A = arrayFromConstructorAndList($Array, O); return sort(A, compareFn); } });

实现要点:

  • 若传入比较函数,先用aCallable校验其可调用性,非法参数直接抛TypeError
  • arrayFromConstructorAndList($Array, O)把原数组元素完整拷进新数组;
  • 最后对副本调用 uncurry 过的原生sort,原数组不受影响。

toSpliced:返回 splice 结果的副本

这是逻辑最复杂的一个。es.array.to-spliced.js 按"参数个数"分三种情况计算插入数与实际删除数:

if (argumentsLength === 0) { insertCount = actualDeleteCount = 0; } else if (argumentsLength === 1) { insertCount = 0; actualDeleteCount = len - actualStart; } else { insertCount = argumentsLength - 2; actualDeleteCount = min(max(toIntegerOrInfinity(deleteCount), 0), len - actualStart); } newLen = doesNotExceedSafeInteger(len + insertCount - actualDeleteCount); A = $Array(newLen);
  • 不传参数:复制一份(删除 0、插入 0);
  • 只传start:删除从actualStart到末尾的全部元素;
  • 传全参数:deleteCounttoIntegerOrInfinity取整,再夹取到[0, len - actualStart]区间;
  • 新长度通过doesNotExceedSafeInteger做安全整数上限校验,防止超长数组导致溢出。

随后用三个循环依次填充:保留段 → 插入段(arguments中的items)→ 尾部剩余段,最终返回全新的toSpliced结果数组。start同样支持负索引(由toAbsoluteIndex处理)。

with:替换单个元素

es.array.with.js 实现替换语义:

var relativeIndex = toIntegerOrInfinity(index); var actualIndex = relativeIndex < 0 ? len + relativeIndex : relativeIndex; if (actualIndex >= len || actualIndex < 0) throw new $RangeError('Incorrect index'); var A = new $Array(len); var k = 0; for (; k < len; k++) createProperty(A, k, k === actualIndex ? value : O[k]);
  • 索引先取整,负数按len + index从尾部定位(-1即最后一个元素);
  • 解析后的索引越界(>= len< 0)时抛出RangeError('Incorrect index')
  • 新数组在目标索引处写入value,其余元素原样复制。

值得注意的一个细节是文件头部的INCORRECT_EXCEPTION_ON_COERCION_FAIL检测(es.array.with.js):它专门探测 Firefox 对"索引强转失败时应抛出的异常"的实现差异,并通过$({ ..., forced: INCORRECT_EXCEPTION_ON_COERCION_FAIL })决定是否强制覆盖原生实现——这正是 core-js 处理引擎行为分歧的典型手法。

%TypedArray% 系列方法:保留类型的复制操作

类型化数组的三个方法由 es.typed-array.to-reversed.js、es.typed-array.to-sorted.js、es.typed-array.with.js 实现,共用的关键模式是:

var A = new (getTypedArrayConstructor(O))(len); // 与原数组同类型的全新 TypedArray

即通过getTypedArrayConstructor取得调用者的具体类型构造器,确保Int16Array#toReversed返回的仍是Int16Array(而不是普通Array),从类型上兑现"按副本变更"的承诺。三个方法的实现分别对应复制反转、复制后原生排序、以及带索引替换的复制。

TypedArray 版本的with还有两个额外的规范细节(es.typed-array.with.js):

  1. BigInt 类型适配:通过isBigIntArray(O)判断目标是否为 BigInt 类型数组,若是则用toBigInt(value)把传入值转成 BigInt,否则用+value做 Number 强转,避免BigInt64Array/BigUint64Array上出现类型错误;
  2. 引擎兼容性探测PROPER_ORDERTHROW_ON_NEGATIVE_FRACTIONAL_INDEX两个探测函数分别检测早期 WebKit 实现中"值强转与索引校验的先后顺序"以及"负小数索引(如-0.5)应截断为 0 而非抛错"的偏差(对应 tc39 提案 PR #86 的语义修正),一旦检测到偏差即通过forced参数强制覆盖原生实现。

Entry Points:按需引入的正确姿势

原文档给出的完整入口列表如下,这也是 README 中 ChangeArrayby copy 一节推荐的引用方式:

core-js/proposals/change-array-by-copy-stage-4 core-js(-pure)/es|stable|actual|full/array(/virtual)/to-reversed core-js(-pure)/es|stable|actual|full/array(/virtual)/to-sorted core-js(-pure)/es|stable|actual|full/array(/virtual)/to-spliced core-js(-pure)/es|stable|actual|full/array(/virtual)/with core-js/es|stable|actual|full/typed-array/to-reversed core-js/es|stable|actual|full/typed-array/to-sorted core-js/es|stable|actual|full/typed-array/with

对各部分的解读:

  • 聚合入口core-js/proposals/change-array-by-copy-stage-4一次性引入提案全部方法。其真实实现是 proposals/change-array-by-copy-stage-4.js,内部逐行requireesnext.array.to-reversedesnext.array.to-sortedesnext.array.to-splicedesnext.array.withesnext.typed-array.to-reversedesnext.typed-array.to-sortedesnext.typed-array.with七个模块。同时该入口已被 stage/4.js 引用,因此引入core-js/stage/4即可自动获得全部 Stage 4 能力;
  • es|stable|actual|full:core-js 的四个引入层级,es(ECMAScript 标准)、stable(含已稳定提案)、actual(含当前活跃提案)、full(全部)。对应仓库 packages/core-js 下的es/stable/actual/full/目录,可按项目对提案激进程度的要求选择;
  • (-pure):表示该入口同时适用于core-jscore-js-pure(后者不污染全局,适合库作者);
  • array(/virtual)/virtual/变体返回"在任意对象上通过.call使用"的方法,适合不想修改Array.prototype的场景;
  • TypedArray 入口无(virtual):与数组方法不同,类型化数组方法以静态导出为主,直接挂在各 TypedArray 子类的原型上。

若你的项目已经按es.*标准模块精细化引入,则可以直接引用core-js/es/array/to-reversed这类单方法入口;而esnext.*变体(见 proposals/change-array-by-copy.js,其中还包含标注了TODO: Remove from core-js@4esnext.typed-array.to-spliced)则是提案未定稿时期的遗留命名。

实战示例

以下用法可直接在支持 core-js 的浏览器或 Node.js 环境中运行(以es入口为例):

// 引入所需 polyfill(按需选择入口) // import 'core-js/es/array/to-reversed'; // import 'core-js/es/array/to-sorted'; // import 'core-js/es/array/to-spliced'; // import 'core-js/es/array/with'; // import 'core-js/es/typed-array/to-reversed'; const arr = [1, 2, 3]; arr.toReversed(); // => [3, 2, 1] arr; // => [1, 2, 3],原数组不变 arr.toSorted((a, b) => b - a); // => [3, 2, 1] arr.toSpliced(1, 1, 4); // => [1, 4, 3] arr; // => [1, 2, 3] arr.with(1, 9); // => [1, 9, 3] arr.with(-1, 0); // => [1, 2, 0],负索引从尾部定位

TypedArray 同样适用,且结果保持原类型:

const typed = new Int8Array([1, 2, 3]); const reversed = typed.toReversed(); // Int8Array [3, 2, 1] reversed instanceof Int8Array; // => true typed; // => Int8Array [1, 2, 3],原视图不变

注意事项与边界行为

  1. 纯函数式承诺:四个方法一律返回新对象、绝不改动原数组/原 TypedArray,这是与reverse/sort/splice/ 索引赋值最本质的区别;
  2. 索引越界with的索引解析后越界会抛RangeError('Incorrect index')toSpliceddeleteCount会被自动夹取到合法区间而非抛错,两者策略不同;
  3. 比较函数校验toSorted传入非函数的comparefn会抛TypeError
  4. Symbol.unscopablestoReversedtoSortedtoSpliced均通过addToUnscopables登记,规避with语句的变量遮蔽问题;
  5. 类数组通用性:Array 系列方法通过toIndexedObject/lengthOfArrayLike支持任意类数组目标(可通过.call使用),TypedArray 系列则严格要求this是 TypedArray 实例(aTypedArray校验);
  6. 引擎偏差兜底:对于 Firefox 的异常强转差异、WebKit 的负小数索引与值强转顺序问题,core-js 通过运行时探测 +forced标记强制覆盖,保证跨引擎行为一致。

小结

「ChangeArrayby copy」是 ECMAScript 近年来少有的、以"返回副本"为设计哲学的标准方法组。core-js 对它的支持横跨标准实现(es.array.*es.typed-array.*模块)与提案入口(change-array-by-copy-stage-4)两个层面,既保证了现代语义的精确还原,又通过esnextstage/4等入口为不同升级节奏的项目提供了灵活的引入方式。理解其签名、实现与边界行为后,你可以在任何受支持的运行环境中放心地把slice().reverse()替换为toReversed(),让代码更简洁、语义更明确。

【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询