Lodash 源码阅读-baseSet
功能概述
baseSet 是 Lodash 内部的核心函数,主要用于根据指定的路径在对象中设置值。它是 set、setWith 等公共 API 的底层实现,支持深层次的属性赋值,并能根据路径动态创建嵌套对象或数组。这个函数还提供了自定义处理器以增强灵活性,可以在设置过程中对值进行转换。
前置学习
依赖函数
- isObject:检查值是否为对象类型,是进行对象操作的前提条件
- castPath:将各种形式的路径(字符串、数组等)转换为标准的路径数组
- toKey:将路径片段转换为有效的对象属性键
- isIndex:判断一个值是否为有效的数组索引
- assignValue:智能地为对象赋值,避免不必要的操作
技术知识
- 对象的深层属性访问:如何在多层嵌套的对象中设置或获取属性
- JavaScript 中的路径表示法:点表示法和方括号表示法的处理
- 动态对象创建:如何基于路径动态创建嵌套的对象结构
- 属性安全性:防止修改特殊属性(如
__proto__、constructor和prototype)
源码实现
function baseSet(object, path, value, customizer) {
if (!isObject(object)) {
return object;
}
path = castPath(path, object);
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
while (nested != null && ++index < length) {
var key = toKey(path[index]),
newValue = value;
if (key === "__proto__" || key === "constructor" || key === "prototype") {
return object;
}
if (index != lastIndex) {
var objValue = nested[key];
newValue = customizer ? customizer(objValue, key, nested) : undefined;
if (newValue === undefined) {
newValue = isObject(objValue)
? objValue
: isIndex(path[index + 1])
? []
: {};
}
}
assignValue(nested, key, newValue);
nested = nested[key];
}
return object;
}
实现思路
- 首先验证输入对象是否为对象类型,非对象则直接返回
- 将输入路径标准化为数组格式
- 循环处理路径的每个部分,逐层深入对象
- 特殊处理危险属性(
__proto__、constructor、prototype)以确保安全 - 对于非最终节点,根据路径的下一部分确定创建对象还是数组
- 使用自定义处理器允许对中间值进行处理
- 通过引用传递逐层深入,最终完成整个路径的赋值
源码解析
1. 参数验证与路径处理
if (!isObject(object)) {
return object;
}
path = castPath(path, object);
首先,函数检查输入的 object 是否为对象类型。如果不是对象,函数直接返回原值,因为无法在非对象上设置属性。
接着,使用 castPath 将路径转换为标准的数组格式。这一步确保函数可以处理多种路径表示形式:
- 数组形式:
['users', 0, 'name'] - 字符串形式:
'users[0].name' - 混合形式:
'users.0.name'
castPath 还会处理一种特殊情况:如果路径作为对象的直接属性存在,则返回包含该路径的单元素数组。
示例:
baseSet({}, "a.b.c", 123); // 路径被转换为 ['a', 'b', 'c']
baseSet({}, ["a", "b", "c"], 123); // 路径保持不变
// 特殊情况
var obj = { "a.b.c": 123 };
baseSet(obj, "a.b.c", 456); // 路径被转换为 ['a.b.c'],而不是 ['a', 'b', 'c']
2. 变量初始化
var index = -1,
length = path.length,
lastIndex = length - 1,
nested = object;
函数初始化了几个关键变量:
index:当前处理的路径索引,初始为-1(之后会递增)length:路径的总长度lastIndex:最后一个路径项的索引nested:当前深入的对象引用,初始指向顶层对象
3. 主循环处理
while (nested != null && ++index < length) {
循环的两个条件:
nested != null:确保当前对象存在(不是null或undefined)++index < length:还有路径部分需要处理
循环的每一轮代表向对象结构深入一层。
4. 获取当前键和处理特殊属性
var key = toKey(path[index]),
newValue = value;
if (key === "__proto__" || key === "constructor" || key === "prototype") {
return object;
}
toKey 函数将路径片段转换为有效的对象键,处理数字、字符串、Symbol 等各种类型的情况。
安全检查:如果发现尝试修改 __proto__、constructor 或 prototype 这些敏感属性,函数会直接返回原对象,防止可能的安全风险(如原型链污染攻击)。
5. 处理中间节点
if (index != lastIndex) {
var objValue = nested[key];
newValue = customizer ? customizer(objValue, key, nested) : undefined;
if (newValue === undefined) {
newValue = isObject(objValue)
? objValue
: isIndex(path[index + 1])
? []
: {};
}
}
当处理的不是路径的最后一项时,函数需要确保当前层级的对象结构正确:
- 获取当前对象下该键的值
objValue - 如果提供了自定义处理器,调用它来处理这个中间值
- 如果没有自定义处理结果,则进行智能判断:
- 如果当前值已经是对象,则保留它
- 如果不是对象,则根据下一级路径决定创建数组还是普通对象:
- 如果下一级路径是有效的数组索引,创建空数组
[] - 否则,创建空对象
{}
- 如果下一级路径是有效的数组索引,创建空数组
这种智能创建方式使得 baseSet 可以根据路径自动构建嵌套结构。
示例:
// 假设 obj = {}
// 设置 obj.users[0].name = 'Alice'
// 过程:
// 1. 检查 obj.users,不存在,创建 {}
// 2. 检查 obj.users[0],不存在,创建 []
// 3. 检查 obj.users[0].name,不存在,设为 'Alice'
6. 赋值与引用更新
assignValue(nested, key, newValue);
nested = nested[key];
使用 assignValue 函数将值赋给当前对象的指定键。assignValue 是一个智能赋值函数,会先检查是否需要更新值,避免不必要的操作。
然后更新 nested 引用,指向刚刚设置的子对象或数组,为下一轮循环深入做准备。
7. 返回结果
return object;
整个过程完成后,返回原始对象。由于 JavaScript 对象是引用类型,通过引用修改的更改已经反映在原对象上。
总结
baseSet 函数是 Lodash 中用于深度设置对象属性的核心实现,总结几个关键技术点:
- 路径标准化处理确保了 API 的灵活性,支持多种路径表示方式
- 防止修改特殊属性保证了安全性,避免潜在风险
- 动态创建对象结构简化了深层嵌套对象的初始化
- 引用传递机制高效地实现了深层属性访问
通过学习这个函数,我们可以掌握高效、安全地操作复杂嵌套对象的关键技术,这在处理配置对象、API 响应数据等场景中非常有用。