es-toolkit/compat 中 get 函数的深度解析:安全路径取值、类型推导与原型污染防护
2026/9/15 18:43:29 网站建设 项目流程

es-toolkit/compat 中 get 函数的深度解析:安全路径取值、类型推导与原型污染防护

【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit

get是 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解决的核心痛点是:在嵌套数据中取值时,任何一层为nullundefined都会导致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.ts:

import { Many } from './Many.ts'; export type PropertyPath = Many<PropertyKey>;

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最重要的特性之一是对空值对象完全安全。当目标对象本身就是nullundefined时,不会抛出异常,而是直接返回默认值:

get(null, 'a.b.c', 'default'); // => 'default' get(undefined, ['a', 'b'], 'default'); // => 'default'

这一行为在源码中有直接体现(src/compat/object/get.ts):

export function get(object: any, path: PropertyKey | readonly PropertyKey[], defaultValue?: any): any { if (object == null) { return defaultValue; } // ... }

object == null同时覆盖nullundefined(宽松相等),从而在最外层就完成了兜底。

四、路径解析的底层原理

get的实现会根据path的类型走不同的分支(src/compat/object/get.ts),核心逻辑可概括为三步:先尝试直接键访问,仅在直接键不存在时才解析为深层路径

4.1 字符串路径:字面键优先于深层路径

path是字符串时,实现会先直接以该字符串为键访问对象(object[path])。只有满足两个条件才会进一步走深层路径解析:

  1. 直接访问结果为undefined
  2. 该字符串被判定为深层键(deep key)且对象上并不存在这个字面键。

判断深层键的逻辑来自 src/compat/_internal/isDeepKey.ts:

const regexIsDeepProp = /\.|(\[(?:[^[\]]*|(["'])(?:(?!\2)[^\\]|\\.)*?\2)\])/;

即只要路径字符串包含点号(.)或形如a[0]a["b"]的方括号访问器,就被视为深层键。空字符串、以点号开头或结尾的字符串不算深层键。

字面键优先这一设计在测试中有明确验证(src/compat/object/get.spec.ts):

const 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']); // => 8

4.3 数组路径:getWithPath 逐段遍历

path是数组时,直接进入getWithPath(src/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.ts):

const 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.ts):

const 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还支持Record<number, 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.ts):

get({ ['__proto__']: {} }, '__proto__', 'defaultValue'); // => 'defaultValue' get({ ['__proto__']: {} }, ['__proto__'], 'defaultValue'); // => 'defaultValue' get({ ['__proto__']: {} }, { toString: () => '__proto__' } as any, 'defaultValue'); // => 'defaultValue'

值得注意的是,该防护仅拦截__proto__这一会改变原型链的键;constructorprototype的读取保持开放(与 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
  • 数字路径重载Record<number, T>number路径的组合;
  • 空对象重载get(null, path, defaultValue)直接推导为TDefault

对于字符串深层路径,最终会落到基于 src/compat/_internal/GetFieldType.ts 的重载上:

export function get<TObject, TPath extends string>( data: TObject, path: TPath ): string extends TPath ? any : GetFieldType<TObject, 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内部的路径分词与逐段遍历。在以下场景中可以完全替代get

get写法原生替代
get(obj, 'a.b.c')obj?.a?.b?.c
get(obj, 'users[0].name')obj?.users?.[0]?.name
get(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[]')1
null值原样返回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 that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询