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.reverse、sort、splice都会原地修改调用者数组。为了不破坏原数据,开发者只能先手动拷贝一份再操作:
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%泛指Int8Array、Uint8Array、BigInt64Array等全部类型化数组子类:
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到末尾的全部元素; - 传全参数:
deleteCount先toIntegerOrInfinity取整,再夹取到[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):
- BigInt 类型适配:通过
isBigIntArray(O)判断目标是否为 BigInt 类型数组,若是则用toBigInt(value)把传入值转成 BigInt,否则用+value做 Number 强转,避免BigInt64Array/BigUint64Array上出现类型错误; - 引擎兼容性探测:
PROPER_ORDER与THROW_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,内部逐行require了esnext.array.to-reversed、esnext.array.to-sorted、esnext.array.to-spliced、esnext.array.with、esnext.typed-array.to-reversed、esnext.typed-array.to-sorted、esnext.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-js与core-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@4的esnext.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],原视图不变注意事项与边界行为
- 纯函数式承诺:四个方法一律返回新对象、绝不改动原数组/原 TypedArray,这是与
reverse/sort/splice/ 索引赋值最本质的区别; - 索引越界:
with的索引解析后越界会抛RangeError('Incorrect index');toSpliced的deleteCount会被自动夹取到合法区间而非抛错,两者策略不同; - 比较函数校验:
toSorted传入非函数的comparefn会抛TypeError; Symbol.unscopables:toReversed、toSorted、toSpliced均通过addToUnscopables登记,规避with语句的变量遮蔽问题;- 类数组通用性:Array 系列方法通过
toIndexedObject/lengthOfArrayLike支持任意类数组目标(可通过.call使用),TypedArray 系列则严格要求this是 TypedArray 实例(aTypedArray校验); - 引擎偏差兜底:对于 Firefox 的异常强转差异、WebKit 的负小数索引与值强转顺序问题,core-js 通过运行时探测 +
forced标记强制覆盖,保证跨引擎行为一致。
小结
「ChangeArrayby copy」是 ECMAScript 近年来少有的、以"返回副本"为设计哲学的标准方法组。core-js 对它的支持横跨标准实现(es.array.*、es.typed-array.*模块)与提案入口(change-array-by-copy-stage-4)两个层面,既保证了现代语义的精确还原,又通过esnext与stage/4等入口为不同升级节奏的项目提供了灵活的引入方式。理解其签名、实现与边界行为后,你可以在任何受支持的运行环境中放心地把slice().reverse()替换为toReversed(),让代码更简洁、语义更明确。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考