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
这些基于它构建的高级功能。