实现 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', '无文章')); // '无文章'