实现 lodash.get
基础知识
lodash.get
的作用是什么?_.get
函数用于安全地根据指定的路径,从一个嵌套对象中获取属性值。如果路径中的任何一个环节不存在(即为null
或undefined
),它不会像原生 JS 那样抛出错误,而是会返回undefined
或一个你指定的默认值。const object = { a: [{ b: { c: 3 } }] }; // 原生方式,如果路径不确定,可能会报错 // console.log(object.a[0].b.d.e); // TypeError: Cannot read properties of undefined // lodash.get 方式,非常安全 // _.get(object, 'a[0].b.c'); // => 3 // _.get(object, 'a[0].b.d.e'); // => undefined // _.get(object, 'a[0].b.d.e', 'default'); // => 'default'
路径 (Path) 的格式
_.get
支持多种路径格式,最常见的是:- 字符串路径: 使用点
.
和方括号[]
来表示层级,例如'a[0].b.c'
。 - 数组路径: 将路径的每一步都作为一个元素放入数组中,例如
['a', '0', 'b', 'c']
。
- 字符串路径: 使用点
核心价值 它的核心价值在于防御性编程。在处理来源不确定的数据(如 API 响应)时,我们无法保证数据结构总是符合预期。使用
_.get
可以让我们编写出更健壮、更简洁的代码,避免大量的&&
或可选链?.
操作符的堆砌。
核心思路
我们的目标是创建一个函数 get(object, path, defaultValue)
,它能模拟原生 _.get
的行为。核心思路可以概括为“循路前进,遇阻则退”:
路径处理: 首先,需要将各种格式的路径(如
'a[0].b.c'
)统一处理成一种标准格式,例如一个由属性键组成的数组['a', '0', 'b', 'c']
。这是最关键的预处理步骤。正则表达式是处理这种字符串的有效工具。迭代取值:
- 将传入的对象
object
作为我们的“当前位置”。 - 遍历标准化的路径数组(
['a', '0', 'b', 'c']
)。 - 在每一步,尝试从“当前位置”取出下一个键对应的属性值(例如,从
object
中取'a'
,再从object.a
中取'0'
...)。 - 将取出的值更新为新的“当前位置”。
- 将传入的对象
安全检查 (遇阻则退): 在迭代的每一步,都必须检查“当前位置”是否为
null
或undefined
。一旦是,就说明路径中断了,无法再继续前进。此时应立即停止遍历,并返回defaultValue
(如果提供了) 或undefined
。返回结果: 如果成功遍历完整个路径数组,那么最后的“当前位置”就是我们想要的目标值,将其返回。
代码实现
下面是一个兼容字符串和数组路径的 get
函数实现。
/**
* 安全地获取嵌套对象的属性值
* @param {object} object - 要查询的对象
* @param {string | Array<string>} path - 属性路径
* @param {any} [defaultValue] - 如果解析值为 undefined,则返回此默认值
* @returns {any} - 返回解析到的值,否则返回默认值或 undefined
*/
function get(object, path, defaultValue) {
// 1. 将字符串路径转换为数组路径
// 正则表达式用于匹配点和方括号,并清理掉多余的字符
const pathArray = Array.isArray(path)
? path
: path.replace(/\[/g, '.').replace(/\]/g, '').split('.');
// 2. 使用 reduce 进行迭代取值
let result = pathArray.reduce((acc, key) => {
// 3. 在每一步都检查 acc 是否为有效的对象
// 如果 acc 是 null 或 undefined,则返回 undefined,中断后续的取值
return acc ? acc[key] : undefined;
}, object); // 4. 初始值 acc 为传入的 object
// 5. 如果最终结果为 undefined,则返回 defaultValue
return result === undefined ? defaultValue : result;
}
代码解析
路径转换(pathArray = ...):
- 数组判断:
Array.isArray(path) ? path : ...
首先检查path
是否为数组,若是则直接使用;若不是则视为字符串路径进行处理。 - 正则预处理:
path.replace(/\[/g, '.')
将所有左方括号[
替换为点号.
,例如'a[0]'
变为'a.0]'
。path.replace(/\]/g, '')
移除所有右方括号]
,例如'a.0]'
变为'a.0'
。
- 分割路径:通过
.split('.')
将处理后的字符串按点号分割成路径键数组,例如'a[0].b.c'
转换为['a', '0', 'b', 'c']
。
- 数组判断:
reduce 迭代(pathArray.reduce(...)):
- 使用 reduce 折叠数组:利用
Array.prototype.reduce
方法将路径数组“折叠”为单一结果,迭代处理每个路径键。 - 回调函数结构:
(acc, key) => ...
,其中acc
为累加器(上一步的取值结果),key
为当前处理的路径键。 - 初始值设置:
reduce
的第二个参数为object
,确保首次迭代时acc
为目标对象,key
为路径数组第一个元素。
- 使用 reduce 折叠数组:利用
安全取值逻辑(acc ? acc[key] : undefined):
- 核心安全检查:每次迭代先判断
acc
是否存在(非null
/undefined
),避免访问undefined
属性时报错。 - 路径中断处理:若
acc
不存在,直接返回undefined
,后续迭代的acc
始终为undefined
,安全终止取值链。 - 取值传递:若
acc
存在,取acc[key]
作为下次迭代的acc
,逐步深入对象属性。
- 核心安全检查:每次迭代先判断
结果处理与默认值返回:
- 获取最终结果:
reduce
执行完毕后,result
为路径最终指向的属性值(若路径完整)或undefined
(若路径中断)。 - 默认值判断:
return result === undefined ? defaultValue : result
,当结果为undefined
时返回用户指定的defaultValue
,否则返回实际取值。
- 获取最终结果:
使用示例
const user = {
id: 1,
info: {
name: '张三',
address: {
city: '北京',
coords: [116.404, 39.915],
},
},
posts: null,
};
console.log(get(user, 'info.name')); // '张三'
console.log(get(user, 'info.address.city')); // '北京'
console.log(get(user, 'info.address.coords[1]')); // 39.915
console.log(get(user, ['info', 'address', 'coords', '0'])); // 116.404
// 测试路径不存在的情况
console.log(get(user, 'info.age')); // undefined
console.log(get(user, 'info.address.zipcode', '邮编未提供')); // '邮编未提供'
// 测试中间路径为 null 的情况
console.log(get(user, 'posts[0].title', '无文章')); // '无文章'