
es-toolkit/compat 中 get 函数的深度解析安全路径取值、类型推导与原型污染防护【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkitget是 es-toolkit 兼容层es-toolkit/compat中与 lodash 完全对齐的对象路径取值函数用于在不编写层层判空代码的前提下安全地从嵌套对象、数组乃至null/undefined中取出目标值并在缺失时回退到默认值。本文将围绕 docs/compat/reference/object/get.md 的官方说明结合 src/compat/object/get.ts 的实现与 src/compat/object/get.spec.ts 的测试用例完整讲解其 API、路径解析规则、类型推导能力、安全防护机制以及官方推荐的性能替代方案。一、get 的定位lodash 兼容层中的安全取值工具es-toolkit 将 API 分为主包es-toolkit与 lodash 兼容包es-toolkit/compat两部分。get属于兼容层目标是与 lodash 行为完全一致方便从 lodash 迁移的现有项目无缝替换。兼容层的导出入口位于 src/compat/compat.ts其中export { get } from ./object/get.ts;将本函数暴露给使用者。get解决的核心痛点是在嵌套数据中取值时任何一层为null或undefined都会导致TypeError。传统写法需要逐层判空代码冗长且易错// 传统写法逐层判空极易出错 const name obj obj.user obj.user.profile obj.user.profile.name;而get将这一过程封装为一次调用import { get } from es-toolkit/compat; const name get(obj, user.profile.name, anonymous);二、API 签名与参数说明函数签名如下const value get(object, path, defaultValue);参数类型说明objectany要查询的对象pathPropertyPath要取值的属性路径可以是字符串、数字、符号symbol或由它们组成的数组defaultValueany可选当解析结果为undefined时返回的默认值返回值any返回解析到的值若路径不存在或值为undefined则返回默认值未提供默认值时返回undefined。其中PropertyPath类型定义于 src/compat/_internal/PropertyPath.tsimport { Many } from ./Many.ts; export type PropertyPath ManyPropertyKey;即PropertyKey | readonly PropertyKey[]PropertyKey是 TypeScript 内置的string | number | symbol联合类型。这解释了为何path可以是字符串点号路径、数字数组索引、符号symbol 键或由它们组成的数组分段路径。三、基本用法四种典型的取值场景官方文档给出了四类最常用的调用方式全部可以通过import { get } from es-toolkit/compat;引入// 1. 点号路径访问嵌套对象 const object { a: { b: { c: 3 } } }; get(object, a.b.c); // 3 // 2. 数组形式的分段路径 get(object, [a, b, c]); // 3 // 3. 为不存在的路径提供默认值 get(object, a.b.d, default); // default // 4. 包含数组索引的路径 const arrayObject { users: [{ name: john }, { name: jane }] }; get(arrayObject, users[0].name); // john第 4 例展示了 bracket 语法users[0].name等价于先取users数组再取下标为0的元素最后取name属性。对 null 和 undefined 的安全访问get最重要的特性之一是对空值对象完全安全。当目标对象本身就是null或undefined时不会抛出异常而是直接返回默认值get(null, a.b.c, default); // default get(undefined, [a, b], default); // default这一行为在源码中有直接体现src/compat/object/get.tsexport function get(object: any, path: PropertyKey | readonly PropertyKey[], defaultValue?: any): any { if (object null) { return defaultValue; } // ... }object null同时覆盖null和undefined宽松相等从而在最外层就完成了兜底。四、路径解析的底层原理get的实现会根据path的类型走不同的分支src/compat/object/get.ts核心逻辑可概括为三步先尝试直接键访问仅在直接键不存在时才解析为深层路径。4.1 字符串路径字面键优先于深层路径当path是字符串时实现会先直接以该字符串为键访问对象object[path]。只有满足两个条件才会进一步走深层路径解析直接访问结果为undefined该字符串被判定为深层键deep key且对象上并不存在这个字面键。判断深层键的逻辑来自 src/compat/_internal/isDeepKey.tsconst regexIsDeepProp /\.|(\[(?:[^[\]]*|([])(?:(?!\2)[^\\]|\\.)*?\2)\])/;即只要路径字符串包含点号.或形如a[0]、a[b]的方括号访问器就被视为深层键。空字符串、以点号开头或结尾的字符串不算深层键。字面键优先这一设计在测试中有明确验证src/compat/object/get.spec.tsconst object { a.b: undefined, a: { b: 99 } }; get(object, a.b); // undefined字面键 a.b 存在虽然值是 undefined get(object, a.b, default); // default以及const object { a.b: 1, a: { b: 2 } }; get(object, a.b); // 1字面键优先于路径4.2 深层路径解析toPath 分词器当确定需要按深层路径解析时实现会调用toPath(path)将路径字符串拆分为段数组再交给getWithPath逐段遍历。src/compat/util/toPath.ts 是一个完整的路径分词器支持以下语法toPath(a.b.c) // [a, b, c] toPath(a[b][c]) // [a, b, c] toPath(a[b.c].d) // [a, b.c, d]引号内的点号属于键的一部分 toPath(a[-1.23]) // [a, -1.23]括号内的数字保持完整 toPath(a..b) // [a, , b]连续点号产生空段对应空字符串键 toPath() // [] toPath(.a.b.c) // [, a, b, c]分词器关键规则包括引号支持[...]或[...]内的内容整体作为一段其中的点号、方括号不再作为分隔符且支持\转义数字保持括号内的数字含负数、小数如[-1.23]按原样保留为一段空段保留连续点号a..b会生成空字符串段用于访问{ a: { : { b: 1 } } }这类含空键的对象未加引号的括号内容含点号时[a.b]会被进一步拆分为[a, b]与 lodash 行为一致。这些边界行为都有对应的测试用例例如// 空括号 a[] 对应空字符串键 const object { a: { : 1 } }; get(object, a[]); // 1 // 连续点号读取空字符串键 get({ a: { : { b: 1 } } }, a..b); // 1 // 复杂路径 const object { a: { -1.23: { [b]: { c: { [d]: { \ne\n: { f: { g: 8 } } } } } } }; get(object, a[-1.23][[\\b\\]].c[\[\\\d\\\]\][\ne\n][f].g); // 8 get(object, [a, -1.23, [b], c, [d], \ne\n, f, g]); // 84.3 数组路径getWithPath 逐段遍历当path是数组时直接进入getWithPathsrc/compat/object/get.ts逐段迭代访问function getWithPath(object: any, path: readonly PropertyKey[], defaultValue?: any): any { if (path.length 0) { return defaultValue; // 空路径直接返回默认值 } let current object; for (let index 0; index path.length; index) { if (current null) { return defaultValue; // 任意一层为空立即兜底 } if (isUnsafeProperty(path[index])) { return defaultValue; // 遇到危险键立即拦截 } current current[path[index]]; } if (current undefined) { return defaultValue; // 最终值为 undefined 时返回默认值 } return current; }注意一个细节数组路径不会被强制转换为字符串。测试验证了这一点src/compat/object/get.spec.tsconst object { a,b,c: 3, a: { b: { c: 4 } } }; get(object, [a, b, c]); // 4而不是 3这与 lodash 保持一致避免了对数组路径的错误拼接。五、数值与符号路径保留 -0 的符号当path是数字或符号时实现会先经 src/compat/_internal/toKey.ts 规范化再直接访问export function toKey(value: unknown): string | symbol { if (typeof value string || typeof value symbol) { return value; } if (Object.is(value?.valueOf?.(), -0)) { return -0; // 保留 -0 的符号 } return String(value); }关键行为是保留-0的符号-0会被规范化为字符串键-0而0会被规范化为0。测试用例src/compat/object/get.spec.tsconst object { -0: a, 0: b }; const props [-0, Object(-0), 0, Object(0)]; props.map(key get(object, key)); // [a, a, b, b]符号路径如get(object, symbol)则直接以符号为键访问支持 symbol-keyed 属性。此外get还支持Recordnumber, T形式的数组类对象get(object, 1)可直接按下标取值src/compat/object/get.ts。六、安全机制原型污染防护get内置了针对原型污染攻击的防护。所有取值路径字符串直取、数组遍历、对象转换在访问前都会调用 src/_internal/isUnsafeProperty.ts 检查export function isUnsafeProperty(key: PropertyKey) { return key __proto__; }当路径段为__proto__时get直接返回默认值从而阻断通过get(obj, __proto__.xxx)触达原型链的攻击面。测试用例src/compat/object/get.spec.tsget({ [__proto__]: {} }, __proto__, defaultValue); // defaultValue get({ [__proto__]: {} }, [__proto__], defaultValue); // defaultValue get({ [__proto__]: {} }, { toString: () __proto__ } as any, defaultValue); // defaultValue值得注意的是该防护仅拦截__proto__这一会改变原型链的键constructor和prototype的读取保持开放与 lodash 一致因为写入路径使用的是更严格的 isUnsafeToWriteProperty见 src/_internal/isUnsafeProperty.ts 注释。七、类型安全重载与 GetFieldType 路径类型推导get的另一个亮点是完善的 TypeScript 类型推导。实现通过大量函数重载src/compat/object/get.ts覆盖各类调用组合深嵌套元组路径支持最多 4 层的[TKey1, TKey2, ...]元组路径逐层推导返回类型如get(obj, [a, b, c])能推导出TObject[a][b][c]可空对象重载当object可能为null | undefined时返回类型自动并上undefined默认值重载提供defaultValue时返回类型变为Exclude真实值, undefined | TDefault数字路径重载Recordnumber, T与number路径的组合空对象重载get(null, path, defaultValue)直接推导为TDefault。对于字符串深层路径最终会落到基于 src/compat/_internal/GetFieldType.ts 的重载上export function getTObject, TPath extends string( data: TObject, path: TPath ): string extends TPath ? any : GetFieldTypeTObject, TPath;GetFieldType是一个纯类型层面的路径解析器能够在编译期解析点号路径a.b.c与括号路径a[b.c]、a[0]并处理数组索引数字或数字字符串、字符串索引以及undefined兜底。这意味着const object { a: { b: { c: 1 } } }; const value get(object, a.b.c); // value 的类型被推导为 number如果路径写错或类型不匹配TypeScript 会在编译期报错而不是等到运行时才发现。八、性能警示为什么官方建议优先使用原生语法官方文档在开篇就以醒目的警告框说明get因复杂的路径解析、null/undefined处理和默认值逻辑而性能较慢建议优先使用现代 JavaScript 语法// 推荐点号/方括号/可选链 const value obj?.a?.b?.c; const value2 obj?.[a]?.[b];可选链optional chaining?.是 ES2020 标准语法运行时开销远低于get内部的路径分词与逐段遍历。在以下场景中可以完全替代getget写法原生替代get(obj, a.b.c)obj?.a?.b?.cget(obj, users[0].name)obj?.users?.[0]?.nameget(obj, a.b, default)obj?.a?.b ?? default注意??对null同样兜底选择建议新项目、新代码优先使用可选链与空值合并运算符??性能更优、类型推导更直接从 lodash 迁移的存量代码、需要PropertyPath字符串路径如路径来自配置文件或运行时动态拼接使用get可以保持行为一致且无需逐处改写需要路径参数化、路径本身是变量或来自外部数据get是更合适的选择因为原生语法无法动态构造路径。九、测试佐证边界行为一览src/compat/object/get.spec.ts 中的测试用例完整覆盖了get的边界行为可作为行为契约参考行为示例结果空路径get({}, , undefined)、get({}, [], a)undefined/a空括号get({ a: { : 1 } }, a[])1null值原样返回get({ a: { b: null } }, a.b)null不是默认值缺段返回默认值get({ a: [, null] }, a[1].b.c)undefined非纯对象可穿透原型链Number.prototype.a { b: 2 }; get(0, a.b)2默认值可为任意类型get(obj, a.b, true)/new Date()//x/原样返回该值其中null值原样返回是一个容易被忽略的细节只有当解析结果严格为undefined时才触发默认值null会被视为有效值原样返回src/compat/object/get.spec.ts。而穿透原型链意味着get在非普通对象如包装对象、类实例上会沿原型链查找属性与 lodash 行为一致src/compat/object/get.spec.ts。结语get是 lodash 兼容层中兼顾安全与兼容的典型函数它通过字面键优先、深层路径分词、逐段遍历、__proto__拦截等机制在运行时实现了与 lodash 完全对齐的取值语义并通过大量重载与GetFieldType类型工具提供了从路径字符串到返回类型的完整推导。理解其实现src/compat/object/get.ts、分词规则src/compat/util/toPath.ts与边界测试src/compat/object/get.spec.ts有助于在迁移 lodash 代码时正确使用并能在性能敏感的路径上果断切换到原生可选链方案。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考