Lodash 源码阅读-stringToPath
概述
stringToPath 是 Lodash 内部的一个小工具函数,它的作用很直接:把字符串形式的属性路径(比如 'a.b.c' 或 'a[0].b')转换成数组形式(比如 ['a', 'b', 'c'] 或 ['a', '0', 'b'])。这个函数是 Lodash 中处理嵌套对象属性访问的基础,让我们可以用字符串来描述如何层层深入一个对象。
前置学习
依赖函数
memoizeCapped:带缓存上限的记忆化函数,防止缓存过大占用过多内存reEscapeChar:一个正则表达式,用于处理转义字符
技术知识
- 属性路径表示法 :JavaScript 中用点号(
.)或方括号([])访问对象属性的语法 - 正则表达式 :用于匹配字符串中复杂模式的工具,这里用
rePropName解析路径 - 记忆化技术:通过缓存函数结果提高重复调用的性能
- 字符编码 :使用
charCodeAt方法判断字符的 ASCII 码值
源码实现
js
var stringToPath = memoizeCapped(function (string) {
var result = [];
if (string.charCodeAt(0) === 46 /* . */) {
result.push("");
}
string.replace(rePropName, function (match, number, quote, subString) {
result.push(
quote ? subString.replace(reEscapeChar, "$1") : number || match
);
});
return result;
});
实现思路
stringToPath 的实现其实挺简单:先创建一个空数组用来存放解析结果,然后检查输入字符串是否以点号开头,如果是(比如 .name),就先放一个空字符串表示从根对象开始。接着用一个精心设计的正则表达式去匹配路径中的每一段,可能是普通属性名(如 name)、数字索引(如 [0] 中的 0)或带引号的属性名(如 ["foo"] 中的 foo)。找到一段就往结果数组里推一个,最后返回整个数组。整个函数还用 memoizeCapped 包了一层,这样对相同的输入字符串就能直接返回之前的结果,不用重复解析,提高性能。
源码解析
记忆化处理
js
var stringToPath = memoizeCapped(function (string) {
// 函数体...
});
这里用 memoizeCapped 包装了整个函数,这样做有什么好处?
- 性能提升:相同的路径字符串只解析一次,后续直接用缓存的结果
- 内存保护 :
memoizeCapped会限制最多缓存 500 个结果,避免内存泄漏
想象一下,如果你的程序中经常需要访问 obj.user.profile.name,每次都从头解析 "user.profile.name" 就很浪费。有了缓存,第一次解析后,后续直接返回 ["user", "profile", "name"]。
处理以点号开头的路径
js
if (string.charCodeAt(0) === 46 /* . */) {
result.push("");
}
为什么要检查第一个字符是不是点号?因为如果路径以点号开头(如 .name),表示从当前对象开始,需要在结果数组中添加一个空字符串作为第一个元素。
js
// 例如:
stringToPath(".name"); // 返回 ['', 'name']
这里用 charCodeAt(0) === 46 而不是 string[0] === '.',是为了提高性能,直接比较字符的 ASCII 码值更快。
使用正则表达式解析各部分
js
string.replace(rePropName, function (match, number, quote, subString) {
result.push(quote ? subString.replace(reEscapeChar, "$1") : number || match);
});
这段代码是整个函数的核心,它利用正则表达式的 replace 方法来遍历并解析路径中的每一段。看起来复杂,我们拆开来看:
string.replace并不是真的要替换什么,而是利用它的回调函数来处理匹配到的每一段rePropName正则表达式能匹配三种路径形式:- 普通属性名:如
user或profile - 数字索引:如
[0]中的0 - 带引号的属性名:如
["foo"]或['bar']中的foo和bar
- 普通属性名:如
- 回调函数接收四个参数:
match:匹配到的完整文本number:如果是数字索引,这里就是数字部分quote:如果是带引号的属性名,这里是引号字符subString:如果是带引号的属性名,这里是引号中间的内容
举个例子:
js
// 路径 "a[0]['b.c']"
// 第一次匹配: match="a", number=undefined, quote=undefined, subString=undefined
// 第二次匹配: match="[0]", number="0", quote=undefined, subString=undefined
// 第三次匹配: match="['b.c']", number=undefined, quote="'", subString="b.c"
// 结果数组: ["a", "0", "b.c"]
对于带引号的属性名,还需要处理可能存在的转义字符(如 \" 或 \'),这就是 subString.replace(reEscapeChar, '$1') 的作用。
返回最终结果
js
return result;
最后返回填充好的结果数组,表示解析完成的属性路径。
总结
stringToPath 虽然是个小函数,但它在 Lodash 中扮演着重要角色,是属性访问相关功能的基础。它的设计体现了几个关键点:
- 实用性:解决了 JavaScript 中访问嵌套属性的常见问题
- 性能优化:通过记忆化缓存避免重复解析相同的路径
- 鲁棒性:能处理各种形式的属性路径表示法
- 内部实现复用:作为基础工具函数被多个 Lodash 方法使用
如果你经常处理复杂的嵌套对象,可以考虑学习 stringToPath 的实现思路,或直接使用 Lodash 提供的 _.get、_.set、_.has 这些基于它构建的高级功能。