:Map / Set / WeakMap / WeakSet 的原生优先策略与 Polyfill 实现解析)
core-js 中的 ECMAScript 集合CollectionsMap / Set / WeakMap / WeakSet 的原生优先策略与 Polyfill 实现解析【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js导读本文以 core-js 官方文档 docs/web/docs/features/ecmascript/collections.md 为骨架系统讲解 core-js 对 ECMAScript 标准集合类型Map、Set、WeakMap、WeakSet的处理策略多数环境下直接复用原生集合、仅在必要时修复方法或构造函数、在老旧环境使用 O(1) 查找的快速 Polyfill。你将掌握每种集合的内建签名、core-js/es|stable|actual|full/...入口点的选用方式、Map.groupBy、Map.getOrInsert以及 Set 运算方法等新特性的完整用法并通过源码级剖析理解其内部的双向链表 哈希索引设计、Weak 集合的隐藏属性存储方案及其注意事项。一、core-js 对集合的核心策略原生优先Polyfill 兜底文档开篇即点明了 core-js 在集合上的总体设计原则core-js 在大多数情况下使用原生集合只在必要时修复方法 / 构造函数并且在老旧环境中使用 O(1) 查找的快速 Polyfill。这可以从 packages/core-js/internals/collection.js 中得到源码级印证。该文件是强集合Map/Set的统一入口逻辑其关键决策点如下检测原生构造器是否可用通过isForced(CONSTRUCTOR_NAME, ...)判断是否需要完全替换。判断条件包括原生构造器是否可调用isCallable(NativeConstructor)、是否支持forEach、以及new NativeConstructor().entries().next()能否正常工作。能力探测并逐方法修复即使原生构造器存在但行为有缺陷core-js 也会逐项探测并打补丁例如早期实现不支持链式调用HASNT_CHAINING即instanceADDER ! instance早期 V8Chromium 40 前后的 Weak 集合在传入原始值时抛异常而不是返回falseTHROWS_ON_PRIMITIVES早期实现对 iterable 构造参数处理不正确ACCEPT_ITERABLES早期实现中-0与0不视为同一个键BUGGY_ZERO。完全不可用时才整体替换当REPLACE为真时调用common.getConstructor(wrapper, CONSTRUCTOR_NAME, IS_MAP, ADDER)生成自研的 Polyfill 构造函数。也就是说core-js 对集合的立场是能用原生就用原生Polyfill 只在原生缺失或行为错误时介入这与许多无条件整体替换的实现形成鲜明对比。二、Map构造函数、groupBy 与 getOrInsert2.1 涉及的模块根据文档Map 相关实现分布在以下模块中位于 packages/core-js/modules/es.map.js模块入口目前只是转发到es.map.constructor注释明确说明该模块将在core-js4中被新模块替代es.map.group-by.jsMap.groupBy静态方法es.map.get-or-insert.jsMap.prototype.getOrInsertes.map.get-or-insert-computed.jsMap.prototype.getOrInsertComputed。2.2 内建签名文档给出的 TypeScript 签名如下class Map { constructor(iterable?: Iterable[key, value]): Map; clear(): void; delete(key: any): boolean; forEach(callbackfn: (value: any, key: any, target: any) void, thisArg: any): void; get(key: any): any; getOrInsert(key: any, value: any): any; getOrInsertComputed(key: any, (key: any) value: any): any; has(key: any): boolean; set(key: any, val: any): this; values(): Iteratorvalue; keys(): Iteratorkey; entries(): Iterator[key, value]; iterator(): Iterator[key, value]; readonly attribute size: number; static groupBy(items: Iterable, callbackfn: (value: any, index: number) key): Mapkey, Arraymixed; }值得注意的细节getOrInsert与getOrInsertComputed属于较新的 ECMAScript 提案Map.prototype.upsert方向在 packages/core-js/internals/map-upsert.js 中可以看到其前身upsert(key, updateFn, insertFn)的实现当键存在时用updateFn(旧值)更新不存在时用insertFn()插入且至少需要一个回调否则抛TypeError。而getOrInsert/getOrInsertComputed语义更简单直接见下。static groupBy是静态方法挂在Map上而非原型上返回Mapkey, Arraymixed。2.3 入口点Entry points文档列出的可用入口如下均遵循core-js(-pure)/es|stable|actual|full/...的规则core-js(-pure)/es|stable|actual|full/map core-js(-pure)/es|stable|actual|full/map/group-by core-js(-pure)/es|stable|actual|full/map/get-or-insert core-js(-pure)/es|stable|actual|full/map/get-or-insert-computed关于入口点的语义可参见 docs/web/docs/usage 中的 Entry points 一节。简要而言es只包含 ECMAScript 标准部分stable包含标准与已稳定的提案actual还包含当前进行中但仍在规范化途中的提案full则尽可能包含全部内容包括 stage 3 之前的实验特性。core-js是全局版本污染全局core-js-pure是纯函数式版本不污染全局。按需引入单个模块比引入整个入口更利于打包体积控制。2.4 示例与行为说明文档给出了完整示例这里保留并补充注释let array [1]; let map new Map([[a, 1], [42, 2]]); map.set(array, 3).set(true, 4); console.log(map.size); // 4 console.log(map.has(array)); // true console.log(map.has([1])); // false // 键按 SameValueZero 比较不是按值比较 console.log(map.get(array)); // 3 map.forEach((val, key) { console.log(val); // 1, 2, 3, 4 console.log(key); // a, 42, [1], true }); map.delete(array); console.log(map.size); // 3 console.log(map.get(array)); // undefined console.log(Array.from(map)); // [[a, 1], [42, 2], [true, 4]] map new Map([[a, 1], [b, 2], [c, 3]]); for (let [key, value] of map) { console.log(key); // a, b, c console.log(value); // 1, 2, 3 } for (let value of map.values()) console.log(value); // 1, 2, 3 for (let key of map.keys()) console.log(key); // a, b, c for (let [key, value] of map.entries()) { console.log(key); // a, b, c console.log(value); // 1, 2, 3 } map Map.groupBy([1, 2, 3, 4, 5], it it % 2); map.get(1); // [1, 3, 5] map.get(0); // [2, 4] map new Map([[a, 1]]); map.getOrInsert(a, 2); // 1 // 键已存在直接返回现有值不覆盖 map.getOrInsert(b, 3); // 3 // 键不存在插入并返回值 map.getOrInsertComputed(a, key key); // 1 // 键已存在回调不会执行 map.getOrInsertComputed(c, key key); // c // 键不存在用回调返回值插入 console.log(map); // Map { a: 1, b: 3, c: c }关键语义要点Map的键使用SameValueZero比较NaN等于NaN-0与0相同所以has([1])为false——两个不同数组对象不相等。Map.groupBy(items, callbackfn)会把可迭代对象按回调返回的组键分组回调接收(value, index)。在 es.map.group-by.js 的实现中可以看到它还额外处理了WebKit bughttps://bugs.webkit.org/show_bug.cgi?id271524IS_PURE模式下或原生Map.groupBy对字符串可迭代对象分组错误时强制打补丁DOES_NOT_WORK_WITH_PRIMITIVES。getOrInsert(key, value)存在即返回现有值不存在则set(key, value)并返回value。实现见 es.map.get-or-insert.js仅两行逻辑if (has(this, key)) return get(this, key); set(this, key, value); return value;。getOrInsertComputed(key, callback)存在即返回现有值回调不执行不存在则调用callback(key)得到新值再插入。实现见 es.map.get-or-insert-computed.js。三、Set集合运算方法的完整实现3.1 涉及的模块文档列出的 Set 模块注意模块文件名带.v2后缀对应文档中链接的文件名包括es.set.jsSet 主体es.set.difference.v2.js差集es.set.intersection.v2.js交集es.set.is-disjoint-from.v2.js是否不相交es.set.is-subset-of.v2.js是否子集es.set.is-superset-of.v2.js是否超集es.set.symmetric-difference.v2.js对称差集es.set.union.v2.js并集。这些运算方法共享 packages/core-js/internals/set-helpers.js、packages/core-js/internals/get-set-record.js 等内部工具。3.2 内建签名class Set { constructor(iterable?: Iterablevalue): Set; add(key: any): this; clear(): void; delete(key: any): boolean; forEach((value: any, key: any, target: any) void, thisArg: any): void; has(key: any): boolean; values(): Iteratorvalue; keys(): Iteratorvalue; entries(): Iterator[value, value]; difference(other: SetLikemixed): Set; intersection(other: SetLikemixed): Set; isDisjointFrom(other: SetLikemixed): boolean; isSubsetOf(other: SetLikemixed): boolean; isSupersetOf(other: SetLikemixed): boolean; symmetricDifference(other: SetLikemixed): Set; union(other: SetLikemixed): Set; iterator(): Iteratorvalue; readonly attribute size: number; }几个要点forEach的回调接收(value, key, target)其中key与value相同Set 的键值同源。七个运算方法的参数类型是SetLikemixed而非严格限定为Set意味着可以接受任何类 Set结构有size、has、keys属性的对象这由GetSetRecord抽象操作支持见下文。所有运算方法返回新集合不会修改原集合。3.3 入口点core-js(-pure)/es|stable|actual|full/set core-js(-pure)/es|stable|actual|full/set/difference core-js(-pure)/es|stable|actual|full/set/intersection core-js(-pure)/es|stable|actual|full/set/is-disjoint-from core-js(-pure)/es|stable|actual|full/set/is-subset-of core-js(-pure)/es|stable|actual|full/set/is-superset-of core-js(-pure)/es|stable|actual|full/set/symmetric-difference core-js(-pure)/es|stable|actual|full/set/union3.4 示例与行为说明let set new Set([a, b, a, c]); set.add(d).add(b).add(e); console.log(set.size); // 5 // 重复的 a、b 自动去重 console.log(set.has(b)); // true set.forEach(it { console.log(it); // a, b, c, d, e }); set.delete(b); console.log(set.size); // 4 console.log(set.has(b)); // false console.log(Array.from(set)); // [a, c, d, e] set new Set([1, 2, 3, 2, 1]); for (let value of set) console.log(value); // 1, 2, 3 for (let value of set.values()) console.log(value); // 1, 2, 3 for (let key of set.keys()) console.log(key); // 1, 2, 3 for (let [key, value] of set.entries()) { console.log(key); // 1, 2, 3 console.log(value); // 1, 2, 3 } new Set([1, 2, 3]).union(new Set([3, 4, 5])); // Set {1, 2, 3, 4, 5} new Set([1, 2, 3]).intersection(new Set([3, 4, 5])); // Set {3} new Set([1, 2, 3]).difference(new Set([3, 4, 5])); // Set {1, 2} new Set([1, 2, 3]).symmetricDifference(new Set([3, 4, 5])); // Set {1, 2, 4, 5} new Set([1, 2, 3]).isDisjointFrom(new Set([4, 5, 6])); // true new Set([1, 2, 3]).isSubsetOf(new Set([5, 4, 3, 2, 1])); // true new Set([5, 4, 3, 2, 1]).isSupersetOf(new Set([1, 2, 3])); // true运算语义速查方法语义示例[1,2,3]与[3,4,5]union并集{1,2,3,4,5}intersection交集{3}difference差集在前者不在后者{1,2}symmetricDifference对称差集仅在一方出现{1,2,4,5}isDisjointFrom是否完全不相交true对[4,5,6]isSubsetOf是否为对方子集true对[5,4,3,2,1]isSupersetOf是否为对方超集true对[1,2,3]3.5 源码中的性能优化细节这些运算方法并非简单双重循环而是根据两侧集合大小选择遍历方向以最小化比较次数unioninternals/set-union.js先克隆this再遍历other逐项add。由于add在底层是 O(1) 哈希索引查找总复杂度约为O(n m)。differenceinternals/set-difference.js先克隆this然后if (size(result) otherRec.size)遍历自身逐项删除否则遍历对方并逐项从自身删除始终选择遍历较小的集合。intersection等其余方法位于 internals/set-intersection.js、internals/set-is-disjoint-from.js、internals/set-is-subset-of.js、internals/set-is-superset-of.js、internals/set-symmetric-difference.js结构类似。而other之所以可以是任意SetLike靠的是 internals/get-set-record.js 中的GetSetRecord抽象操作它读取obj.sizeNaN或负数会抛RangeError/TypeError并要求has与keys可调用从而把任何满足接口的对象统一包装成带getIterator()与includes(it)的记录。四、WeakMap弱引用映射与私有字段存储4.1 涉及的模块es.weak-map.jsWeakMap 主体同样转发到es.weak-map.constructores.weak-map.get-or-insert.jsWeakMap.prototype.getOrInsertes.weak-map.get-or-insert-computed.jsWeakMap.prototype.getOrInsertComputed。4.2 内建签名class WeakMap { constructor(iterable?: Iterable[key, value]): WeakMap; delete(key: object | symbol): boolean; get(key: object | symbol): any; getOrInsert(key: object | symbol, value: any): any; getOrInsertComputed(key: object | symbol, (key: any) value: any): any; has(key: object | symbol): boolean; set(key: object | symbol, val: any): this; }注意与Map不同WeakMap的键被限定为object | symbol没有size、没有clear也不可迭代。4.3 入口点core-js(-pure)/es|stable|actual|full/weak-map core-js(-pure)/es|stable|actual|full/weak-map/get-or-insert core-js(-pure)/es|stable|actual|full/weak-map/get-or-insert-computed4.4 示例基本操作与私有字段let a [1]; let b [2]; let c [3]; let weakmap new WeakMap([[a, 1], [b, 2]]); weakmap.set(c, 3).set(b, 4); console.log(weakmap.has(a)); // true console.log(weakmap.has([1])); // false // 键是对象引用不是值比较 console.log(weakmap.get(a)); // 1 weakmap.delete(a); console.log(weakmap.get(a)); // undefined // 用 WeakMap 存储私有属性 let Person (() { let names new WeakMap(); return class { constructor(name) { names.set(this, name); } getName() { return names.get(this); } }; })(); let person new Person(Vasya); console.log(person.getName()); // Vasya for (let key in person) console.log(key); // 只有 getName实例上没有可枚举的私有字段WeakMap 键必须为对象或 symbol这一约束在 internals/collection.js 的修复逻辑中有体现fixMethod中delete/get/has对IS_WEAK !isObject(key)的情况直接返回false/undefined而不是抛错——这是为了兼容早期 V8 中 Weak 集合对原始值抛异常的错误行为。五、WeakSet弱引用集合5.1 涉及的模块es.weak-set.js主体同样转发到es.weak-set.constructor。5.2 内建签名class WeakSet { constructor(iterable?: Iterablevalue): WeakSet; add(key: object | symbol): this; delete(key: object | symbol): boolean; has(key: object | symbol): boolean; }WeakSet 是四类集合中最精简的只有add/delete/has无迭代、无size、无clear。5.3 入口点core-js(-pure)/es|stable|actual|full/weak-set5.4 示例let a [1]; let b [2]; let c [3]; let weakset new WeakSet([a, b, a]); weakset.add(c).add(b).add(c); console.log(weakset.has(b)); // true console.log(weakset.has([2])); // false weakset.delete(b); console.log(weakset.has(b)); // false六、源码级实现原理从双向链表到弱引用元数据6.1 强集合 Polyfill哈希索引 双向链表当原生集合不可用而整体替换时core-js 使用 packages/core-js/internals/collection-strong.js 生成构造函数。其核心数据结构是双链表的哈希表每个内部状态internals/internal-state.js保存{ index, first, last, size }index是一个以null为原型的对象充当哈希索引表键为fastKey计算出的键 IDfirst/last是双向链表的头尾指针用于保持插入顺序迭代顺序即插入顺序每个 entry 结构为{ index, key, value, previous, next, removed }删除时只是标记removed true并从链表摘除见clear/delete的实现删掉的节点不会立刻释放而是延迟到迭代器掠过时回退。size通过内部状态维护在支持描述符的环境用访问器属性暴露defineBuiltInAccessor(Prototype, size, ...)不支持时则退化为普通属性that.size。键的哈希由 packages/core-js/internals/internal-metadata.js 的fastKey(it, create)计算原始值直接加前缀字符串S it、数字等P it、symbol 原样返回对象若尚未打元数据可扩展则打上uid(meta)生成的隐藏属性并返回objectID形如O0、O1…不可扩展冻结对象返回F不需要创建时返回E因此getEntry走两条路index ! F时直接从索引表取O(1)冻结对象回退为从头遍历链表比较引用。这正好印证了文档O(1) 查找的快速 Polyfill的论断——绝大多数键通过哈希索引一次命中仅冻结对象退化为线性扫描。6.2 Weak 集合 Polyfill键上的隐藏属性存储弱集合无法在旧环境真正弱引用core-js 采用把值存储为键对象的隐藏属性的方式见 packages/core-js/internals/collection-weak.js 与internal-metadata.js的getWeakData每个集合实例有自增id为键对象打上隐藏的METADATA属性weakData[state.id] value即完成存储冻结不可扩展对象无法打隐藏属性于是回退到UncaughtFrozenStore——一个简单的[key, value]数组存储collection-weak.js 中uncaughtFrozenStore。为不暴露元数据键enable()会补丁Object.getOwnPropertyNames在结果中剔除METADATA键。这正是文档末尾 [!WARNING]警告的由来Weak 集合的 Polyfill 把值作为键的隐藏属性存储。大多数情况下它工作正确且不会泄漏但最好让集合的存活时间长于其键。原生 symbol 作为WeakMap键时无法在不产生内存泄漏的前提下被正确 Polyfill。结合源码解释若键先于集合被 GC隐藏属性随键一起被回收数据自然丢失且不可恢复反过来若集合先被回收而键还活着隐藏属性会成为永久滞留的引用。因此约定集合活得比键久。同时 symbol 是原始值无法挂载隐藏属性所以基于该方案的 Polyfill 无法支持 symbol 键的弱语义——这也是文档特意提示的限制。6.3 模块入口的兼容性处理packages/core-js/modules/es.map.js 等薄壳模块只做一件事use strict; // TODO: Remove this module from core-js4 since its replaced to module below require(../modules/es.map.constructor);即把es/map入口转发到es.map.constructor为将来core-js4的模块重组预留了迁移路径。这说明该仓库在集合模块的组织上正在向构造函数模块独立演进。七、总结与使用建议回到文档的核心观点core-js 在集合领域原生优先、按需修复、极端情况才整体替换。这对使用者意味着入口点选择日常使用core-js/es/map或core-js/stable/set这类入口即可覆盖标准与稳定提案需要Map.groupBy、Map.getOrInsert等最新特性时可按需引入core-js/full/map/group-by、core-js/full/map/get-or-insert等细分入口或直接使用full入口入口点规则详见文档。注意 Polyfill 环境差异在缺失原生集合的极老环境如 IE8-10中强集合会退化为哈希表 双向链表实现Weak 集合则依赖键隐藏属性方案此时应遵守集合寿命长于键的使用约定并避免依赖原生 symbol 作为 Weak 键。新方法语义需熟记getOrInsert/getOrInsertComputed只在键不存在时写入可替代先has再set的惯用法避免重复查找Set 的七个运算方法均返回新集合且可接受任意SetLike对象。通过本文的源码对照internals/collection.js、internals/collection-strong.js、internals/collection-weak.js、internals/set-difference.js 等你可以更自信地在项目中评估 core-js 集合模块的行为边界并基于入口点机制按需裁剪引入的代码量。【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考