首页
/ Angular 框架源码中 Object.create(null) 的取舍准则:原型碰撞防护的 PR 评审规则与实践

Angular 框架源码中 Object.create(null) 的取舍准则:原型碰撞防护的 PR 评审规则与实践

2026-09-05 14:45:38作者:温玫谨Lighthearted

在 Angular 核心框架仓库中,把普通对象字面量 {} 替换为 Object.create(null) 是一类看似无害、实则极易出错的重构。Angular 仓库的 PR 评审流程为此沉淀了一套专门的评审规则文档,明确了 Object.create(null){}Map 三者的适用边界。本文完整继承这套规则并对照仓库源码逐一印证:读完后你将掌握一套可直接用于代码评审的判定清单——什么场景必须防原型碰撞、什么场景替换了反而引入性能或兼容性隐患,以及公开 API 对象该如何安全处理动态键。

规则文档的定位:PR 评审流程中的专项参考

这套规则并不是孤立的笔记,而是 Angular 仓库 Agent 辅助 PR 评审技能(.agent/skills/pr_review/)中的专项参考文档。PR 评审指南在“Key Focus Areas”中明确要求:审查涉及各包的修改时,优先查阅 reference/ 目录下的专题规则,并特别指出——当 PR 将 {} 替换为 Object.create(null) 时,必须对照 object_create_null.md 中的技术评估标准与规则。这背后的动机在评审指南的开头即可看出:Angular 是核心框架,任何改动都可能影响大量下游开发者,因此对“原型链剥离”这类隐蔽的行为变更设置了专门的评审闸门。

适用条件:三个前提同时满足才值得用 Object.create(null)

规则的第一节给出了明确的正向判定:只有当对象同时满足以下三个条件时,才适合使用 Object.create(null)(或 Map):

  1. 该对象被用作内部键值查找表或集合(internal key-value lookup map or set);
  2. 键是任意的、不受信任的动态字符串——规则文档点名的典型来源包括 $locationShim 的 URL 查询参数、HTML sanitizer 的标签集合、以及 jsaction 的 DOM 事件类型解析器;
  3. 属性存在性检查采用直接索引或 in 检查(如 map[key] !== undefinedkey in map),使得键一旦撞上 Object.prototype 的成员名('toString''constructor''hasOwnProperty' 等)就会产生误报命中或错误行为

仓库中的真实用例印证

上述三个条件在 Angular 源码中都有对应的真实实现,且都集中在“动态字符串键 + 存在性判断”这一形态上:

  • HTML 安全 schema(Sanitizer)dom_security_schema.ts 中定义 const createNullObj = () => Object.create(null);,并以此构建三级嵌套的 SECURITY_SCHEMA(属性名 → 命名空间 → 标签名 → SecurityContext)。schema 的键来自属性/标签名这类字符串集合,而 html_sanitizer.ts 中构建 BooleanRecord 布尔查找表时也两次使用 Object.create(null)——这正是“键集合 + 存在性检查”的典型形态。
  • Router 的 URL 段树:路由子路径以任意字符串为键,create_url_tree.tsurl_tree.ts 中均用 const children: {[key: string]: UrlSegmentGroup} = Object.create(null); 来存放 UrlSegmentGroupapply_redirects.ts 的重定向应用同理。子路径名理论上可以是 toStringconstructor 等任意字符串,用空原型对象可保证 key in children 的判断不被原型链污染。
  • $locationShim 的查询参数解析location_shim.tsconst searchObj: {[key: string]: unknown} = Object.create(null); 用于解析 URL query string——查询参数完全由外部 URL 决定,是典型的“不受信任动态键”。
  • jsaction 事件类型解析:Angular 实验性事件派发机制中的 jsaction 属性把“DOM 事件类型 → 处理函数名”编码为字符串(如 jsaction="click:confirmPurchase"),其解析缓存见 action_resolver.ts(将 jsaction 属性值解析为“事件 → 限定名”的 map 并缓存),属性语法说明见 event-dispatch README。事件类型名来自 HTML 属性,属于外部输入,符合“不可信动态键”条件。

公开 API 与边界对象:绝不要盲目剥离原型

规则第二节针对一个高风险场景:当对象接收不受信任的动态键同时又暴露给公共消费方或第三方代码时(规则点名的例子是 ngOnChanges 生命周期钩子中的 SimpleChanges),把对象改成 Object.create(null)禁止的盲目操作

原因在于:剥离 Object.prototype 属于破坏性 API 变更。消费方代码如果调用 .hasOwnProperty().toString().valueOf(),或使用字符串插值(`${obj}`),会在运行时直接抛出 TypeError: obj.hasOwnProperty is not a function

规则给出的安全替代方案有四条,均可落地:

  1. Object.hasOwn(obj, key):框架内部做属性查找时改用 Object.hasOwn,替代直接索引或 in 检查。这样在不破坏对象原型、不影响消费方的前提下,就能防止内部读取时的原型碰撞。
  2. 输入键消毒(Input Key Sanitization):在填充对象时过滤或删除危险键名(__proto__constructorprototype)。
  3. Map 或专用类:对需要动态键键值存储的新公共 API,优先选用 Map<K, V>,或提供显式 .get() / .has() 方法的专用类。
  4. 走废弃/破坏性变更流程:如果确有必要把某个公开对象的原型改为 null,必须遵循 Angular 正式的 deprecation 与 major 版本破坏性变更流程——这与 PR 评审指南中“Breaking changes require strict approval processes and deprecation periods”的要求一致。

这条规则的实质是把“原型碰撞防护”与“API 兼容性”解耦:防护责任应该放在读取侧Object.hasOwnMap),而不是通过改变存储侧的隐式契约(对象的 prototype)来实现。

禁用场景:五类不应替换为 Object.create(null) 的对象

规则的第三节列举了不应把 {} 换成 Object.create(null) 的五种场景,每一条都有明确的技术理由,且在仓库源码中可以找到对应形态的实例:

  1. 固定形状的 Struct 与 DTO:属性名是硬编码静态字符串的对象(如文档示例 let sortedBreakpoints: {breakpoints?: number[]} = {})。理由:Object.assign({}, ...) 只拷贝自身的可枚举属性,源对象上的原型属性永远不会被拷贝,因此此类对象不存在原型碰撞问题,替换毫无收益。
  2. 数字键映射:以数字为键索引的对象(如 tasksByHandleId: {[id: number]: Task})。数字键与 Object.prototype 的字符串成员名不冲突,key in map 的误报问题天然不存在。
  3. 引用哨兵(Reference Sentinels):纯粹用于引用同一性判断的对象,如 const EMPTY_OBJECT = {}const IN_PROGRESS_RESOLUTION = {}。仓库中确实存在这两类用法:animation_transition_factory.ts 定义了 EMPTY_OBJECT = {}ngtsc scope local.ts 定义了 IN_PROGRESS_RESOLUTION = {}。这类对象的“身份”才是语义,原型有无成员完全无关。
  4. 编译器内部 AST 与 Visitor 状态:键由框架内部生成、不受信任用户输入无法投毒键名的临时对象。
  5. 性能敏感路径与体积敏感包:这是最容易被忽视的一条。标准 {} 字面量能享受 V8 的快速隐藏类(fast hidden classes)单态内联缓存(monomorphic inline caching);而 Object.create(null) 会把对象直接推进 V8 的字典模式(dictionary mode),放弃这些优化,同时还会增加压缩后的包体积。规则点名的场景是内联 polyfill(如 event-dispatch-contract)和 SSR 水合包。仓库中与之对应的构建痕迹包括 event-dispatch 的 contract 二进制打包产物(其中将 __jsaction_bootstrap 挂到 window 上)以及 SSR 水合集成测试中的 copy-event-dispatch-contract.mjs——这些内联进最终产物的脚本对体积和初始化性能都极其敏感,恰是“不要为了理论上的安全性牺牲运行时性能”的典型位置。

评审应用要点速查

把三个小节合并成一份可在 PR 评审中逐条勾选的清单:

判定步骤 检查点 结论倾向
1 对象是否为内部查找表/集合,且键来自不受信任的动态字符串? 是 → 考虑 Object.create(null) / Map;否 → 不替换
2 存在性检查是否为 map[key] / key in map 形式? 是 → 原型碰撞风险真实存在;否 → 替换收益存疑
3 对象是否暴露给公共 API 或第三方代码? 是 → 禁止直接改为空原型对象,改用 Object.hasOwn、键消毒、Map/专用类,或走 deprecation 流程
4 是否属于固定形状 DTO、数字键表、引用哨兵、编译器内部状态? 是 → 不替换(见五类禁用场景)
5 是否位于热路径或体积敏感产物(内联 polyfill、SSR 水合包)? 是 → 不替换,权衡 V8 字典模式与包体积代价

这套规则的精髓在于:Object.create(null) 在 Angular 这类框架中不是“更干净的写法”,而是一种有明确触发条件和明确代价的防御性手段。它的收益(消除原型链上的键名碰撞)只在“不可信字符串键 + 存在性检查”的窄口径下成立;而它的代价(V8 字典模式、包体积、公共 API 破坏)在其余场景下只会是净损失。对照仓库中的真实用法——Sanitizer 的 SECURITY_SCHEMA、Router 的 URL 段树$locationShimquery 解析 使用空原型对象,而 EMPTY_OBJECTIN_PROGRESS_RESOLUTION 等哨兵对象保持普通字面量——可以看到这套评审标准与 Angular 源码现状是完全自洽的。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384