core-js 相对索引方法(Relative Indexing Method)完整指南:Array / String / TypedArray 的.at()负索引读取
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
导读
相对索引方法(Relative Indexing Method)是 TC39 提出的 ECMAScript 提案,为Array、String和%TypedArray%引入统一的at(index)方法,允许通过负索引从末尾读取元素,终结了arr[arr.length - 1]这类冗长写法的历史。本指南以 docs/web/docs/features/proposals/relative-indexing-method.md 为骨架,结合 core-js 仓库中es.array.at、es.string.at-alternative、es.typed-array.at三个模块的源码与单元测试,讲解该提案在 core-js 中的实现原理、内置方法签名、入口使用方式及边界行为。读完你将掌握.at()的完整语义、core-js 的 polyfill 加载入口,以及如何用单元测试验证 polyfill 的兼容行为。
提案背景与 core-js 中的定位
相对索引方法(Relative Indexing Method)的规范与提案仓库分别见 tc39 规范 与 提案仓库。它解决的核心痛点在于:传统下标arr[-1]在 JavaScript 中返回undefined,开发者只能写arr[arr.length - 1]来取末位元素,可读性差且容易出错。.at()让负索引"从末尾倒数",语义直观且统一。
在 core-js 中,该提案当前处于 Stage 4(已进入 ES2022 正式规范),因此:
- 它同时出现在
core-js/proposals入口(对应提案入口)与core-js/stage入口(对应Stage 4 集合)中; - 在 packages/core-js/stage/4.js 中可以看到
require('../proposals/relative-indexing-method')的引入语句; - 由于已入标准,
Array.prototype.at、String.prototype.at、%TypedArray%.prototype.at也都以es.*前缀(标准模块)形式存在于 packages/core-js/modules 目录。
从源码结构看,core-js 对已进 Stage 4 的提案采用了"双入口"策略:既保留
proposals/入口方便按提案名精确引入,也纳入stage/集合统一引入,最终随着正式版发布迁移为es.*标准模块。
内置方法签名(Built-ins signatures)
文档给出了三种内置对象的at方法签名,核心语义一致:
class Array { at(index: int): any; } class String { at(index: int): string; } class %TypedArray% { at(index: int): number; }三个方法的参数都是整数index,返回值类型分别为any(数组元素)、string(字符串字符)和number(类型化数组元素)。签名背后的统一算法如下:
- 将
this强制转换为可索引对象(Object / String / TypedArray); - 取得其长度
len; - 将
index经ToIntegerOrInfinity抽象操作转换为整数relativeIndex; - 若
relativeIndex >= 0,则k = relativeIndex;否则k = len + relativeIndex(负索引从末尾倒数); - 若
k < 0 || k >= len,返回undefined,否则返回O[k]。
这一算法在三份实现中几乎逐行一致,下面结合源码逐一分析。
源码级实现:三个at模块的异同
1.Array.prototype.at— packages/core-js/modules/es.array.at.js
$({ target: 'Array', proto: true }, { at: function at(index) { var O = toObject(this); var len = lengthOfArrayLike(O); var relativeIndex = toIntegerOrInfinity(index); var k = relativeIndex >= 0 ? relativeIndex : len + relativeIndex; return (k < 0 || k >= len) ? undefined : O[k]; } }); addToUnscopables('at');关键点:
toObject(this):将this包装为对象,保证at.call({ 0: 1, length: 1 }, 0)这类"类数组对象"调用也能工作;lengthOfArrayLike(O):读取对象的length,兼容类数组;toIntegerOrInfinity(index):把参数转成整数(见下文"内部抽象操作");addToUnscopables('at'):将at加入Array.prototype[Symbol.unscopables],避免with (arr) { at }这类历史遗留语法被新方法污染。
2.String.prototype.at— packages/core-js/modules/es.string.at-alternative.js
var charAt = uncurryThis(''.charAt); var FORCED = fails(function () { return '𠮷'.at(-2) !== '\uD842'; }); $({ target: 'String', proto: true, forced: FORCED }, { at: function at(index) { var S = toString(requireObjectCoercible(this)); var len = S.length; var relativeIndex = toIntegerOrInfinity(index); var k = relativeIndex >= 0 ? relativeIndex : len + relativeIndex; return (k < 0 || k >= len) ? undefined : charAt(S, k); } });与数组版本的区别:
forced: FORCED特性探测:通过fails检测运行环境自带的String.prototype.at是否符合规范——检测用例是'𠮷'.at(-2) !== '\uD842'。'𠮷'是占两个 UTF-16 码元(surrogate pair)的字符,at(-2)规范上应返回第一个码元'\uD842'。如果宿主实现按"码点"而非"码元"取字符(返回'𠮷'),说明实现有误,core-js 就会强制覆盖为自身 polyfill;uncurryThis(''.charAt):以非包装方式借用原生charAt,避免在String子类或代理对象上产生额外开销;- 返回越界时同样是
undefined,与数组行为对齐。
3.%TypedArray%.prototype.at— packages/core-js/modules/es.typed-array.at.js
exportTypedArrayMethod('at', function at(index) { var O = aTypedArray(this); var len = lengthOfArrayLike(O); var relativeIndex = toIntegerOrInfinity(index); var k = relativeIndex >= 0 ? relativeIndex : len + relativeIndex; return (k < 0 || k >= len) ? undefined : O[k]; });该模块通过ArrayBufferViewCore.exportTypedArrayMethod一次性为所有类型化数组子类(Int8Array、Uint16Array、Float64Array等 9 种视图)注册at方法,并用aTypedArray(this)做类型强校验,确保只作用于真正的 TypedArray 实例。返回的仍是number类型的元素值,越界返回undefined。
补充:在
proposals入口中,TypedArray 部分目前通过 packages/core-js/modules/esnext.typed-array.at.js 间接引用es.typed-array.at(文件头部有// TODO: Remove from core-js@4注释),说明该入口属于提案时代的遗留转发,标准实现始终是es.typed-array.at。
内部抽象操作:toIntegerOrInfinity与参数规范化
三份实现都调用了 packages/core-js/internals/to-integer-or-infinity.js:
var trunc = require('../internals/math-trunc'); module.exports = function (argument) { var number = +argument; return number !== number || number === 0 ? 0 : trunc(number); };该内部模块实现规范中的ToIntegerOrInfinity抽象操作,行为要点:
- 先把参数一元取正(
+argument),转成 number; NaN与±0统一归零(number !== number || number === 0 ? 0);- 其余值用
Math.trunc截断小数部分。
由此推导出.at()的几个实用边界行为(均可由 tests/unit-global/es.array.at.js 中的断言证实):
[1, 2, 3].at(NaN)→1(NaN 归 0,即取第一个元素);[1].at()→1(无参数时undefined转 NaN,同样归 0);[1, 2, 3].at(-0)→1(-0 >= 0成立,按正索引 0 处理);[1, 2, 3].at(0.4)/at(0.5)/at(0.6)→ 均为1(小数被截断为 0)。
边界行为与测试验证
core-js 的单元测试 tests/unit-global/es.array.at.js 使用 QUnit 对Array#at做了系统验证,几乎逐条覆盖上文算法:
assert.same([1, 2, 3].at(0), 1); assert.same([1, 2, 3].at(3), undefined); // 越界返回 undefined assert.same([1, 2, 3].at(-1), 3); // 负索引从末尾倒数 assert.same([1, 2, 3].at(-4), undefined); // 负索引越界 assert.same([1, 2, 3].at(0.4), 1); // 小数截断 assert.same([1].at(NaN), 1); assert.same([1].at(), 1); assert.same([1, 2, 3].at(-0), 1); assert.same(Array(1).at(0), undefined); // 稀疏空位返回 undefined assert.same(at.call({ 0: 1, length: 1 }, 0), 1); // 类数组对象可用 assert.true('at' in Array.prototype[Symbol.unscopables]); // unscopables 注册严格模式下还断言了对null/undefined调用会抛TypeError。归纳出的行为规则如下:
| 调用形式 | 结果 | 说明 |
|---|---|---|
arr.at(i),i >= 0且i < len | arr[i] | 正索引按位取 |
arr.at(-1) | 末位元素 | len - 1 |
arr.at(-len) | 首位元素 | 恰好倒数到 0 |
arr.at(i),i >= len或i < -len | undefined | 越界不抛错 |
arr.at(0.5)/arr.at(NaN)/arr.at() | 等价于arr.at(0) | 参数被ToIntegerOrInfinity规范化 |
at.call(null)/at.call(undefined) | TypeError | 严格模式下不可索引 |
对String#at和%TypedArray%#at的越界行为完全一致:返回undefined,不抛异常,也不改变原对象。
Entry points:如何在项目中引入
文档给出的提案入口为:
core-js/proposals/relative-indexing-method在代码中使用时对应:
import 'core-js/proposals/relative-indexing-method'; // 或 CommonJS require('core-js/proposals/relative-indexing-method');该入口文件 packages/core-js/proposals/relative-indexing-method.js 的实现非常简洁,本质是三个模块的聚合转发:
'use strict'; // https://github.com/tc39/proposal-relative-indexing-method require('../modules/es.string.at-alternative'); require('../modules/esnext.array.at'); require('../modules/esnext.typed-array.at');由于该提案已经进入 ES2022 正式规范,core-js 还提供了多种等价或更细粒度的引入方式,可按需选择:
- 按模块细粒度引入(体积最小):
core-js/es/array/at、core-js/es/string/at、core-js/es/typed-array/at,分别对应标准模块 es.array.at.js、es.string.at-alternative.js、es.typed-array.at.js; - 按类别引入:
core-js/es/array、core-js/es/string、core-js/es/typed-array; - 按 Stage 引入:
core-js/stage(或core-js/stage/4),其中 packages/core-js/stage/4.js 已包含require('../proposals/relative-indexing-method'); - 全量引入:
core-js(完整 polyfill)或core-js/stable(仅标准特性,已包含.at())。
关于入口语义的完整说明,可参阅仓库文档 docs/web/docs/usage 中的 "Entry points" 小节。选用建议:若只依赖.at()一个特性且在意包体积,直接用core-js/es/array/at等三个细粒度入口;若项目已统一按core-js/stable全量引入,则无需额外引入。
与其他取元素方式的对比
| 方式 | 取末位元素 | 越界行为 | 备注 |
|---|---|---|---|
arr[arr.length - 1] | 繁琐 | 返回undefined | 需先取length,可读性差 |
arr.slice(-1)[0] | 需再解包 | 返回undefined | 产生新数组,有分配开销 |
arr.at(-1) | 直接、直观 | 返回undefined | 提案语义,不产生新对象 |
在类型化数组上,typedArray.at(-1)同样避免了typedArray[typedArray.length - 1]的样板代码;而String.prototype.at(-1)则省去了str[str.length - 1]。这就是相对索引方法统一三类的意义:一份直觉(负索引倒数),三种内置类型通用。
小结
- 相对索引方法为
Array、String、%TypedArray%提供统一的at(index)方法,负索引从末尾倒数,越界返回undefined,参数经由ToIntegerOrInfinity规范化(NaN、±0 归零,小数截断); - core-js 的三份实现共享同一套核心算法,
String#at额外通过forced特性探测修正不规范的宿主实现; - 该提案已是 Stage 4(ES2022),可按
core-js/proposals/relative-indexing-method提案入口、core-js/es/*/at标准模块入口或core-js/stage集合入口引入; - 实现与边界行为均有源码(es.array.at.js、es.string.at-alternative.js、es.typed-array.at.js)与单元测试(tests/unit-global/es.array.at.js)双重背书,可在引入前按上表行为表自行验证运行环境的兼容性。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考