es-toolkit/compat 的 result 函数:沿路径取值并自动调用函数的 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
导读
result是es-toolkit/compat提供的 Lodash 兼容函数,它在对象中沿给定路径取值,但有一个关键差异:路径上遇到的每一个函数都会被自动调用,最终值如果是函数也会被执行并返回其结果。本指南将结合 result 官方文档 与 result 源码实现、测试用例,完整讲解其用法、参数、this绑定规则、路径解析细节、边界行为,以及它与get函数、可选链(?.)的取舍建议,帮助你在迁移 Lodash 代码时快速、准确地使用这一能力。
函数签名与基本语义
const result = result(obj, path, defaultValue);result的行为可以概括为三步:
- 沿
path在object中逐段取值; - 每取到一个值,如果它是函数,就以当前所处对象为
this调用它,并用返回值继续沿路径前进; - 如果某一段取到
undefined,则返回defaultValue(若默认值本身是函数,则调用它并把结果作为返回值)。
它的类型签名如下(见 result.ts):
export function result<R>( object: any, path: PropertyPath, defaultValue?: R | ((...args: any[]) => R) ): R;其中PropertyPath定义为Many<PropertyKey>,即可以是单个PropertyKey(字符串、数字、symbol),也可以是它们的数组(见 _internal/PropertyPath.ts)。
基础用法与示例
普通值取值(与get相同)
当路径上不存在函数时,result的行为与普通取值一致:
import { result } from 'es-toolkit/compat'; const obj = { a: { b: { c: 3 } } }; const value = result(obj, 'a.b.c'); // Result: 3自动调用函数
这是result的核心能力:最终值如果是函数,会被自动调用并返回调用结果:
const objWithFunc = { compute: () => ({ value: 42 }), getValue: function () { return this.compute().value; }, }; const computed = result(objWithFunc, 'getValue'); // Result: 42 (getValue function is called)路径中途的函数也会被调用
result不仅调用最终值,路径上每一步遇到的函数都会被调用,然后用其返回值继续解析后续路径段:
const nested = { data: () => ({ user: { getName: () => 'John' } }), }; const name = result(nested, 'data.user.getName'); // Result: 'John' (both data() and getName() are called)使用默认值
当解析结果为undefined时返回默认值:
const incomplete = { a: { b: null } }; const withDefault = result(incomplete, 'a.b.c', 'default value'); // Result: 'default value'默认值是函数时会被调用
与路径上的函数一样,如果传入的defaultValue本身是函数,result会调用它并返回调用结果:
const withFuncDefault = result(incomplete, 'a.b.c', () => 'computed default'); // Result: 'computed default' (default value function is called)使用数组路径
路径既可以是字符串(如'a.b.c'),也可以是键数组(如['a', 'b', 'c']):
const arrayPath = result(objWithFunc, ['getValue']); // Result: 42动态默认值
由于默认值可以是函数,因此可以实现"按需生成"的兜底值,比如带上当前时间戳:
const dynamic = result(incomplete, 'missing.path', function () { return `Generated at ${new Date().toISOString()}`; }); // Result: string with current timethis 绑定:函数调用时保留调用上下文
result在调用函数时,会以当前遍历到的对象作为this绑定。这意味着对象方法中可以安全地引用自身属性:
import { result } from 'es-toolkit/compat'; const calculator = { multiplier: 2, compute: function () { return 10 * this.multiplier; }, }; const calculatedValue = result(calculator, 'compute'); // Result: 20 (this.multiplier is correctly referenced)从源码看,这一行为对应 result.ts 中的value.call(object):每解析一段路径,就把object更新为当前节点,再以它作为后续函数调用的this。测试用例 result.spec.ts 也专门验证了深层方法中this的绑定正确性:
const value = { a: { b: function () { return this.c; }, c: 1, }, }; // result(value, 'a.b') === 1参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
object | any | 要查询的对象。可以是普通对象、数组、乃至数字/字符串等原始值(配合原型链上的属性) |
path | PropertyPath | 要获取属性的路径。可以是字符串(如'a.b.c'、'a[0].b')、单个键,或键数组(如['a', 'b', 'c']),也支持 symbol 键 |
defaultValue | R \| ((...args: any[]) => R),可选 | 当解析结果为undefined时返回的值;如果是函数则调用它并返回结果 |
返回值(R):解析出的最终值。路径上的函数会被调用,最终值若为函数也会被调用并返回其结果。
源码级原理:路径解析与循环取值
result的实现非常精简,核心逻辑集中在 result.ts:
export function result<R>(object: any, path: PropertyPath, defaultValue?: R | ((...args: any[]) => R)): R { if (isKey(path, object)) { path = [path]; } else if (!Array.isArray(path)) { path = toPath(toString(path)); } const pathLength = Math.max(path.length, 1); for (let index = 0; index < pathLength; index++) { const value = object == null ? undefined : object[toKey(path[index])]; if (value === undefined) { return typeof defaultValue === 'function' ? (defaultValue as any).call(object) : (defaultValue as R); } object = typeof value === 'function' ? value.call(object) : value; } return object; }整个流程可以分为三个阶段:
1. 路径归一化
- 如果
path是单个键(由 _internal/isKey.ts 判定),直接包装成单元素数组; - 否则如果
path不是数组,则通过toPath(toString(path))把字符串路径解析成路径段数组。
isKey的判定规则(isKey.ts)包括:数组一律不是单键;数字、布尔值、null/undefined、symbol 一律视为单键;字符串则只有当它是纯单词字符(/^\w*$/)或不包含深度路径标记(.、[...])时才视为单键。这意味着result(obj, 'a.b')会被当作深度路径解析,而如果对象自身恰好拥有'a.b'这个字面键,isKey还会通过Object.hasOwn兜底判定(见下文"键优先于路径"的边界行为)。
2. 逐段取值与函数调用
主循环逐段解析路径,每一轮都执行"取值 → 判空 → 调函数":
object == null ? undefined : object[toKey(path[index])]:安全取值,null/undefined对象不会抛错;toKey(_internal/toKey.ts)负责把路径段规范化为字符串或 symbol 键,并保留-0的符号(Object.is(value?.valueOf?.(), -0)时返回'-0');- 值若为函数,则
value.call(object)以当前对象为this调用它,结果成为下一轮的对象。
3. 兜底默认值
一旦某段解析结果为undefined,立即返回默认值:默认值是函数则(defaultValue as any).call(object)调用之,否则直接返回原值。Math.max(path.length, 1)保证了空路径也能正常执行一次循环(此时取到的是object[toKey(undefined)],即undefined,从而走默认值分支)。
字符串路径的解析细节
result复用了 compat 层共享的toPath(src/compat/util/toPath.ts)来解析字符串路径,支持多种书写形式:
toPath('a.b.c') // ['a', 'b', 'c'] toPath('a[b][c]') // ['a', 'b', 'c'] toPath('.a.b.c') // ['', 'a', 'b', 'c'] toPath('a["b.c"].d') // ['a', 'b.c', 'd'](引号内的点不会被拆分) toPath('') // [] toPath('.a[b].c.d[e]["f.g"].h') // ['', 'a', 'b', 'c', 'd', 'e', 'f.g', 'h']解析器是一个带状态的逐字符扫描器,核心规则包括:
.作为分隔符;连续点或结尾点会产生空段('a..b'→['a', '', 'b']);[...]括号内的内容作为一个整体段;引号包裹("..."或'...')内的点不会被拆分,例如'["a.b"]'保持为'a.b';- 括号内未加引号且含点、且不是数字的内容仍按点拆分(如
'[a.b]'→['a', 'b']),与 Lodash 行为一致; - 括号内的数字(含负数、小数,如
'[-1.23]')整体保留; - 支持反斜杠转义(
'a["b\\"c"]'中的引号转义)。
边界行为与测试验证
result的行为在 result.spec.ts 中有全面覆盖,这些边界情况对迁移 Lodash 代码非常关键:
键优先于路径
如果对象自身拥有与路径字符串字面相同的键,则该键优先:
const object = { 'a.b': 1, a: { b: 2 } }; result(object, 'a.b'); // 1(字面键优先) result(object, ['a.b']); // 1(数组路径同样按字面键处理)数组路径不会被强转成字符串
const object = { 'a,b,c': 3, a: { b: { c: 4 } } }; result(object, ['a', 'b', 'c']); // 4(而不是 3)保留-0的符号
const object = { '-0': 'a', 0: 'b' }; result(object, -0); // 'a' result(object, Object(-0)); // 'a' result(object, 0); // 'b' result(object, Object(0)); // 'b'symbol 键
const object = {}; object[symbol] = 1; result(object, symbol); // 1空路径与空括号
result({}, ''); // undefined result({ '': 3 }, ['']); // 3 result({ a: { '': 1 } }, 'a[]'); // 1(空括号作为空段)复杂路径
深层嵌套、含特殊字符的键也能正确解析:
const object = { a: { '-1.23': { '["b"]': { c: { "['d']": { '\ne\n': { f: { g: 8 } } } } } } }, }; result(object, 'a[-1.23]["[\\"b\\"]"].c[\'[\\\'d\\\']\'][\ne\n][f].g'); // 8 result(object, ['a', '-1.23', '["b"]', 'c', "['d']", '\ne\n', 'f', 'g']); // 8nullish 对象与缺失路径
result(null, 'constructor'); // undefined(不抛错) result(undefined, ['constructor']); // undefined result({ a: [, null] }, 'a[1].b.c'); // undefined(路径中段缺失)可以返回 null
只有当值为undefined时才触发默认值,null会被原样返回:
const object = { a: { b: null } }; result(object, 'a.b'); // null(不是默认值)非普通对象的路径查找
路径解析不限定于普通对象,原型链上的属性同样可达(测试中通过修改Number.prototype验证):
// 修改 numberProto.a = { b: 2 } 后: result(0, 'a.b'); // 2空路径且带默认值
result({}, [], 'a'); // 'a'与 get、可选链的对比与取舍建议
官方文档在 result 参考文档 开头明确给出警告:result因复杂的路径处理与函数调用逻辑而性能较慢,建议优先使用更快的get函数或可选链(?.)。
三者的能力对比如下:
| 能力 | result | get | 可选链?. |
|---|---|---|---|
| 深度路径取值 | ✅ | ✅ | ✅ |
| 自动调用路径上的函数 | ✅ | ❌(原样返回函数) | ❌ |
| 返回函数调用结果 | ✅ | ❌ | ❌ |
| 默认值支持 | ✅(可为函数) | ✅ | 需手动??组合 |
| 路径形式 | 字符串 / 数组 / 键 | 字符串 / 数组 / 键 | 仅静态属性访问 |
| 性能 | 较慢(复杂解析 + 函数调用) | 更快 | 最快(引擎原生) |
从源码结构也可以印证这一取舍:get在 src/compat/object/get.ts 中针对字符串、数字、symbol、数组等路径类型做了分派优化,并且没有函数调用逻辑;而result的循环里每一段都要做一次"是否函数"的判断与可能的call。此外get还通过isUnsafeProperty(见 _internal/isUnsafeProperty.ts)对__proto__、constructor、prototype等危险属性做了防护,result则没有这一层检查。
实践建议:
- 绝大多数场景(纯取值 + 默认值)应使用
get或可选链; - 仅当你的代码确实依赖"沿路径自动调用函数"这一 Lodash 语义(例如从配置对象中按路径取出 getter 并立即执行)时,才选用
result; - 迁移 Lodash 存量代码时,
result用于保证行为 1:1 兼容;新代码则推荐直接改写成get或?.以获得更优性能。
在 compat 包中的定位与导入方式
result归属于es-toolkit/compat的 object 模块,并从 src/compat/compat.ts 统一导出。与 compat 下所有函数一样,它支持两种导入方式:
// 从主入口导入 import { result } from 'es-toolkit/compat'; // 独立入口导入(在无 tree-shaking 的环境下更省体积) import result from 'es-toolkit/compat/result';es-toolkit/compat旨在与 Lodash 的接口和行为 1:1 对齐(自 v1.39.3 起通过 Lodash 自身测试套件),详见 compat 介绍文档,因此result的调用方式与 Lodash 完全一致,可以无缝替换lodash/result。需要注意的是,result属于 compat 层保留的"带有隐式行为"的函数,在严格 API 的es-toolkit主入口中并不提供——如果你正在从 Lodash 迁移,应在清理调用点后逐步改用es-toolkit主入口的get与可选链。
小结
result是 Lodash 兼容层中一个"取值 + 执行函数"二合一的特殊工具:它沿PropertyPath逐段解析,自动调用路径上遇到的每个函数并保留this绑定,在遇到undefined时返回(可执行的)默认值。通过 result.ts 的实现与 result.spec.ts 的 20 余项测试,你可以确认它在键/路径优先级、-0符号、symbol 键、空路径、复杂路径、nullish 对象等边界行为上与 Lodash 完全一致。但在追求性能的现代代码中,请优先考虑get或可选链,仅在确实需要自动调用函数语义时才使用result。
【免费下载链接】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),仅供参考