Lodash源码阅读-baseSet

75 阅读5分钟

Lodash 源码阅读-baseSet

功能概述

baseSet 是 Lodash 内部的核心函数,主要用于根据指定的路径在对象中设置值。它是 setsetWith 等公共 API 的底层实现,支持深层次的属性赋值,并能根据路径动态创建嵌套对象或数组。这个函数还提供了自定义处理器以增强灵活性,可以在设置过程中对值进行转换。

前置学习

依赖函数

  • isObject:检查值是否为对象类型,是进行对象操作的前提条件
  • castPath:将各种形式的路径(字符串、数组等)转换为标准的路径数组
  • toKey:将路径片段转换为有效的对象属性键
  • isIndex:判断一个值是否为有效的数组索引
  • assignValue:智能地为对象赋值,避免不必要的操作

技术知识

  • 对象的深层属性访问:如何在多层嵌套的对象中设置或获取属性
  • JavaScript 中的路径表示法:点表示法和方括号表示法的处理
  • 动态对象创建:如何基于路径动态创建嵌套的对象结构
  • 属性安全性:防止修改特殊属性(如__proto__constructorprototype

源码实现

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__constructorprototype)以确保安全
  • 对于非最终节点,根据路径的下一部分确定创建对象还是数组
  • 使用自定义处理器允许对中间值进行处理
  • 通过引用传递逐层深入,最终完成整个路径的赋值

源码解析

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:确保当前对象存在(不是 nullundefined
  • ++index < length:还有路径部分需要处理

循环的每一轮代表向对象结构深入一层。

4. 获取当前键和处理特殊属性

var key = toKey(path[index]),
  newValue = value;

if (key === "__proto__" || key === "constructor" || key === "prototype") {
  return object;
}

toKey 函数将路径片段转换为有效的对象键,处理数字、字符串、Symbol 等各种类型的情况。

安全检查:如果发现尝试修改 __proto__constructorprototype 这些敏感属性,函数会直接返回原对象,防止可能的安全风险(如原型链污染攻击)。

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])
      ? []
      : {};
  }
}

当处理的不是路径的最后一项时,函数需要确保当前层级的对象结构正确:

  1. 获取当前对象下该键的值 objValue
  2. 如果提供了自定义处理器,调用它来处理这个中间值
  3. 如果没有自定义处理结果,则进行智能判断:
    • 如果当前值已经是对象,则保留它
    • 如果不是对象,则根据下一级路径决定创建数组还是普通对象:
      • 如果下一级路径是有效的数组索引,创建空数组 []
      • 否则,创建空对象 {}

这种智能创建方式使得 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 中用于深度设置对象属性的核心实现,总结几个关键技术点:

  1. 路径标准化处理确保了 API 的灵活性,支持多种路径表示方式
  2. 防止修改特殊属性保证了安全性,避免潜在风险
  3. 动态创建对象结构简化了深层嵌套对象的初始化
  4. 引用传递机制高效地实现了深层属性访问

通过学习这个函数,我们可以掌握高效、安全地操作复杂嵌套对象的关键技术,这在处理配置对象、API 响应数据等场景中非常有用。