Lodash 源码阅读-hasPath
概述
hasPath
是 Lodash 的内部工具函数,用于检查对象中是否存在指定路径。它是 _.has
和 _.hasIn
这两个公共 API 的核心实现,能够处理各种格式的路径查询并妥善处理各种边界情况。
前置学习
依赖函数
- castPath :将各种格式的路径转换成标准数组格式,例如
"a.b.c"
转为["a", "b", "c"]
- toKey:将路径中的各个部分转换为有效的属性键,特别处理了负零(-0)的情况
- isLength:判断一个值是否是合法的数组长度(非负整数且小于等于最大安全整数)
- isIndex:判断一个值是否是合法的数组索引(非负整数且小于给定的长度)
- isArray:检查一个值是否为数组
- isArguments:检查一个值是否为函数的 arguments 对象
技术知识
- 属性路径 :JavaScript 中表示嵌套对象路径的方式,如
user.profile.name
或user['profile']['name']
- 对象属性访问:JavaScript 中通过点号或方括号访问对象属性的方法
- 数组索引和稀疏数组:数组索引的特性及稀疏数组(含"空位"的数组)的概念
- 类数组对象:具有数字索引和 length 属性但不是真正数组的对象
源码实现
javascript
function hasPath(object, path, hasFunc) {
path = castPath(path, object);
var index = -1,
length = path.length,
result = false;
while (++index < length) {
var key = toKey(path[index]);
if (!(result = object != null && hasFunc(object, key))) {
break;
}
object = object[key];
}
if (result || ++index != length) {
return result;
}
length = object == null ? 0 : object.length;
return (
!!length &&
isLength(length) &&
isIndex(key, length) &&
(isArray(object) || isArguments(object))
);
}
实现思路
hasPath
的实现分为两个主要阶段:
-
路径逐级查找 :首先将路径转换为标准数组格式,然后从第一级开始逐级检查对象中是否存在该属性。如果任一级别查找失败,立即返回
false
。 -
特殊数组处理:如果常规查找在最后一级失败,函数会进行额外检查:判断当前对象是否是数组或类数组对象,以及最后一级路径是否是一个有效的数组索引。这主要是为了处理数组中的"空位"情况。
源码解析
路径准备
javascript
path = castPath(path, object);
var index = -1,
length = path.length,
result = false;
这部分代码完成初始化工作:
-
使用
castPath
将路径转换为标准数组格式,处理不同的输入形式:javascriptcastPath("a.b.c", obj); // 转为 ['a', 'b', 'c'] castPath(["a", "b", "c"], obj); // 保持不变 castPath("a.b", { "a.b": 42 }); // 转为 ['a.b'],因为这是直接属性
-
设置初始变量:
index
: 当前处理的路径级别,初始为 -1length
: 路径数组的长度result
: 查找结果,默认为false
路径查找循环
javascript
while (++index < length) {
var key = toKey(path[index]);
if (!(result = object != null && hasFunc(object, key))) {
break;
}
object = object[key];
}
这个循环是函数的核心部分:
++index < length
确保循环在路径范围内进行toKey(path[index])
将当前路径段转换为有效属性键object != null
检查当前对象是否存在(不是null
或undefined
)hasFunc(object, key)
使用传入的函数检查键是否存在于对象中- 如果检查通过,更新
result
为true
,并深入下一层object = object[key]
- 如果检查失败,设置
result
为false
并退出循环
hasFunc
是从外部传入的属性检查函数:
_.has
使用baseHas
(只检查对象自身属性)_.hasIn
使用baseHasIn
(检查对象自身及继承属性)
示例:
javascript
var user = {
profile: {
name: "张三",
},
};
// 检查 user.profile.name 是否存在
hasPath(user, ["profile", "name"], (obj, key) => key in obj);
// 第一轮:检查 'profile' 是否存在于 user 中 ✓
// 第二轮:检查 'name' 是否存在于 user.profile 中 ✓
// 结果:true
特殊情况处理
javascript
if (result || ++index != length) {
return result;
}
length = object == null ? 0 : object.length;
return (
!!length &&
isLength(length) &&
isIndex(key, length) &&
(isArray(object) || isArguments(object))
);
这部分处理两种情况:
-
如果路径查找成功(
result
为true
)或未完成整个路径的遍历(++index != length
),直接返回result
-
如果查找恰好在最后一级失败,进行特殊检查:
- 检查当前对象是否有
length
属性且值有效 - 检查
length
是否是合法的数组长度 - 检查最后一个键是否是合法的数组索引(在 0 到 length-1 范围内)
- 确认当前对象是否是数组或类数组对象
- 检查当前对象是否有
这个特殊处理主要是为了处理数组的"空位"情况:
javascript
var arr = [];
arr[2] = "值"; // arr 现在是 [empty, empty, "值"]
// 检查 arr[1] 是否存在
hasPath(arr, [1], (obj, key) => key in obj);
// 常规检查:false(索引 1 是空的)
// 特殊检查:
// - arr.length 存在且为 3 ✓
// - 3 是合法的数组长度 ✓
// - 键 1 是合法的索引(在 0-2 范围内)✓
// - arr 是数组 ✓
// 最终结果:true
总结
hasPath
作为 Lodash 属性检查功能的核心实现,展示了几个重要的编程技巧:
- 输入标准化:将不同格式的输入转换为统一格式,增强函数的兼容性
- 逐级探索:通过循环逐步检查属性路径,发现问题立即终止,提高效率
- 边界情况处理:对数组的特殊处理,确保在复杂情况下也能得到符合预期的结果
- 函数参数化:通过传入不同的检查函数,实现灵活的属性检查策略
这些技巧在处理复杂数据结构、支持多种输入格式或需要精确控制行为时非常有用。通过 _.has
和 _.hasIn
这两个公共 API,我们可以利用 hasPath
的功能编写更加健壮和安全的代码,避免因属性访问错误导致的程序崩溃。