Lodash源码阅读-baseIsMatch

Lodash 源码阅读-baseIsMatch

概述

baseIsMatch 是 Lodash 内部的一个工具函数,专门检查一个对象是否"包含"另一个对象的所有属性和值。简单来说,它判断对象 A 是不是有对象 B 的全部"零件",而且这些"零件"要一模一样。它是 _.isMatch 方法的内部实现,支持深度比较和自定义比较规则,效率还特别高。

前置学习

依赖函数

  • Stack:一个特殊的栈数据结构,专门记录已经比较过的对象,防止循环引用导致的无限递归(就是 A 引用 B,B 又引用 A 这种死循环情况)
  • baseIsEqual:Lodash 内部的深度比较函数,负责判断两个值是否相等,能处理各种复杂类型的比较
  • getMatchData:从源对象提取属性数据,将对象的键值对转换成特定格式的数组,并标记每个值是否可以用简单方式比较

技术知识

  • 短路求值:遇到不匹配立刻返回结果,不继续比较,节省时间
  • 双循环优化:巧妙设计两个循环,第一个循环做快速检查,第二个循环做深度检查
  • 对象属性访问:用in操作符检查属性是否存在,用点或中括号获取属性值
  • 自定义比较器:支持用户提供特殊的比较规则,增加灵活性
  • 位运算标志:用按位或|组合不同的比较模式标志

源码实现

javascript 复制代码
function baseIsMatch(object, source, matchData, customizer) {
  var index = matchData.length,
    length = index,
    noCustomizer = !customizer;

  if (object == null) {
    return !length;
  }
  object = Object(object);
  while (index--) {
    var data = matchData[index];
    if (
      noCustomizer && data[2]
        ? data[1] !== object[data[0]]
        : !(data[0] in object)
    ) {
      return false;
    }
  }
  while (++index < length) {
    data = matchData[index];
    var key = data[0],
      objValue = object[key],
      srcValue = data[1];

    if (noCustomizer && data[2]) {
      if (objValue === undefined && !(key in object)) {
        return false;
    } else {
      }
      var stack = new Stack();
      if (customizer) {
        var result = customizer(objValue, srcValue, key, object, source, stack);
      }
      if (
        !(result === undefined
          ? baseIsEqual(
              srcValue,
              objValue,
              COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG,
              customizer,
              stack
            )
          : result)
      ) {
        return false;
      }
    }
  }
  return true;
}

实现思路

baseIsMatch 的实现很聪明,分成三个主要步骤:

  1. 先看有没有东西比 :首先检查目标对象是不是 nullundefined,如果是空的,那就只有在没有属性需要匹配时才算匹配成功。

  2. 快速初筛 :第一轮循环做快速检查,对那些可以用 === 直接比较的属性(比如数字、字符串),直接用不等号比;对于复杂类型,就先看属性存不存在。只要发现一个不匹配,立刻返回 false

  3. 深入比较:第二轮循环做详细比较,尤其是对象和数组这类复杂类型:

    • 对于简单类型,再检查一下 undefined 的特殊情况
    • 对于复杂类型,要么用自定义比较器,要么用 baseIsEqual 进行深度递归比较

这种设计既保证了比较结果的准确性,又通过"先简单后复杂"的策略大大提高了性能。

源码解析

函数入参详解

baseIsMatch 函数接收四个参数:

  1. object: 要检查的目标对象

    • 这是我们要分析的主体,判断它是否包含源对象的所有属性和值
    • 可以是任何类型,包括基本类型、对象、数组等
    • 如果是 nullundefined,会有特殊处理逻辑
  2. source: 提供匹配属性的源对象

    • 包含了我们想要在目标对象中找到的属性和值
    • 实际比较中并不直接使用,而是通过 matchData 间接使用
    • 主要用于自定义比较器的调用
  3. matchData: 匹配数据数组

    • getMatchData(source) 预处理生成
    • 是一个特殊格式的数组,每个元素也是一个数组,形如 [key, value, isStrictComparable]
      • key: 属性名
      • value: 属性值
      • isStrictComparable: 布尔值,表示该值是否可用 === 严格比较
    • 预处理源对象可以提高比较效率,避免重复判断值的类型
  4. customizer: 自定义比较器函数(可选)

    • 一个可选的函数,允许自定义比较逻辑
    • 函数签名为:function(objValue, srcValue, key, object, source, stack)
    • 返回值:
      • 如果返回 undefined,则使用默认的 baseIsEqual 进行比较
      • 如果返回任何其他值,该返回值将决定比较结果(真值表示匹配,假值表示不匹配)
    • 增加了极大的灵活性,允许用户实现特殊的比较逻辑,如模糊匹配、范围比较等

初始化和空值检查

javascript 复制代码
var index = matchData.length,
  length = index,
  noCustomizer = !customizer;

if (object == null) {
  return !length;
}
object = Object(object);

这段代码先初始化几个变量:

  • indexlength 都设为 matchData 的长度,一个用来遍历,一个保存原始长度
  • noCustomizer 用来标记是否没有自定义比较器,等于 !customizer

然后检查 object 是不是 nullundefined

  • 如果是空值,只有在 matchData 为空数组(没有要匹配的属性)时才返回 true
  • 否则,调用 Object(object) 确保 object 是对象类型,这样即使传入原始类型如数字、字符串也能正常工作

举个例子:

javascript 复制代码
// 如果 object 是 null,且没有属性需要匹配
baseIsMatch(null, {}, [], null); // 返回 true

// 如果 object 是 null,但有属性需要匹配
baseIsMatch(null, { a: 1 }, [["a", 1, true]], null); // 返回 false

// 如果 object 是原始类型,会被转换为对象
baseIsMatch(
  123,
  {
    valueOf: function () {
      return 123;
    },
  },
  [
    [
      "valueOf",
      function () {
        return 123;
      },
      false,
    ],
  ],
  null
);
// 会比较 Number(123) 和源对象

快速路径检查

javascript 复制代码
while (index--) {
  var data = matchData[index];
  if (
    noCustomizer && data[2] ? data[1] !== object[data[0]] : !(data[0] in object)
  ) {
    return false;
  }
}

这个循环从后往前遍历 matchData 数组,对每个匹配项做快速检查:

  • data[0] 是属性名,例如 "name"
  • data[1] 是源对象中的值,例如 "张三"
  • data[2] 是个布尔值,表示这个值能不能用 === 简单比较

条件判断的逻辑是:

  • 如果没有自定义比较器且值可以简单比较(noCustomizer && data[2]为真),就直接判断值是否不相等(data[1] !== object[data[0]]
  • 否则,就只检查这个属性是不是存在(!(data[0] in object)

只要有一个属性检查失败,立刻返回 false

来看个例子:

javascript 复制代码
// 假设有以下对象和matchData
var obj = { name: "张三", age: 30, friends: ["李四", "王五"] };
var matchData = [
  ["name", "张三", true], // 字符串,可以简单比较
  ["age", 30, true], // 数字,可以简单比较
  ["friends", ["李四", "王五"], false], // 数组,不能简单比较
];

// 第一轮循环检查 (index = 2)
// 检查 "friends" 是否存在于 obj 中 (因为data[2]为false)
// "friends" in obj 为 true,所以继续

// 第二轮循环检查 (index = 1)
// 直接比较 obj.age === 30 (因为data[2]为true)
// 相等,所以继续

// 第三轮循环检查 (index = 0)
// 直接比较 obj.name === "张三" (因为data[2]为true)
// 相等,通过快速检查

深度比较

javascript 复制代码
while (++index < length) {
  data = matchData[index];
  var key = data[0],
    objValue = object[key],
    srcValue = data[1];

  if (noCustomizer && data[2]) {
    if (objValue === undefined && !(key in object)) {
      return false;
    }
  } else {
    var stack = new Stack();
    if (customizer) {
      var result = customizer(objValue, srcValue, key, object, source, stack);
    }
    if (
      !(result === undefined
        ? baseIsEqual(
            srcValue,
            objValue,
            COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG,
            customizer,
            stack
          )
        : result)
    ) {
      return false;
    }
  }
}

这个循环从头开始再次遍历 matchData 数组,进行深度比较:

  1. 对于可以简单比较的值(noCustomizer && data[2]为真):

    • 主要处理 undefined 的特殊情况
    • 检查 objValue === undefined && !(key in object),区分"属性不存在"和"属性值为 undefined"的情况
  2. 对于需要深度比较的值或有自定义比较器的情况:

    • 创建一个新的 Stack 实例,用来记录比较过的对象,防止循环引用
    • 如果有自定义比较器,调用它并获取结果
    • 如果自定义比较器返回 undefined(表示没有明确结果),则使用 baseIsEqual 进行深度比较
    • 比较时使用了 COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG 位掩码,表示:
      • COMPARE_PARTIAL_FLAG(值为 1):开启部分比较模式,源对象可以是目标对象的子集
      • COMPARE_UNORDERED_FLAG(值为 2):开启无序比较模式,数组元素顺序可以不同

来看个实际例子:

javascript 复制代码
// 深度比较对象
var user = {
  name: "张三",
  profile: {
    age: 30,
    city: "北京",
    hobbies: ["篮球", "编程"],
  },
};

var criteria = {
  profile: {
    age: 30,
    hobbies: ["篮球", "编程"],
  },
};

// getMatchData 会将 criteria 转换为约 [["profile", {age:30, hobbies:[...]}, false]]
// 在深度比较中,会比较 user.profile 和 criteria.profile
// 使用 baseIsEqual 递归比较这两个对象的结构和值
// 因为设置了 COMPARE_PARTIAL_FLAG,所以 criteria.profile 可以是 user.profile 的子集
// 即使 user.profile 有额外的 city 属性,比较仍然会成功

// 最终结果
baseIsMatch(user, criteria, getMatchData(criteria), null); // 返回 true

对于自定义比较器的例子:

javascript 复制代码
// 自定义比较器示例,允许年龄在正负5岁范围内匹配
function ageRangeCustomizer(objValue, srcValue, key) {
  if (key === "age") {
    return Math.abs(objValue - srcValue) <= 5;
  }
  // 返回 undefined 表示使用默认比较
}

var person = { name: "李四", age: 32 };
var query = { name: "李四", age: 30 };

// 在没有自定义比较器时,这会返回 false,因为32 !== 30
// 但有了自定义比较器,32和30的差值在5以内,所以返回 true
baseIsMatch(person, query, getMatchData(query), ageRangeCustomizer); // 返回 true

返回结果

javascript 复制代码
return true;

如果所有属性检查都通过了(没有提前返回 false),函数最后返回 true,表示目标对象完全匹配源对象的所有指定属性。

总结

baseIsMatch 虽然是个内部函数,但它的设计思想非常值得学习:

  1. 性能优化

    • 两次循环分别处理简单和复杂情况,简单的先检查,能快速失败就快速失败
    • 用短路求值避免不必要的深度比较
    • 对可以用 === 比较的值进行特殊处理,避免创建不必要的 Stack 实例
  2. 灵活性

    • 支持自定义比较器,让用户能定义自己的比较逻辑
    • 特殊处理各种边界情况,如 nullundefined
    • 支持深度嵌套对象的比较
  3. 正确性

    • 使用 Stack 处理循环引用问题,防止无限递归
    • 区分"属性不存在"和"属性为 undefined"的情况
    • 配合 baseIsEqual 支持复杂数据结构的精确比较

实际上,JavaScript 的对象比较一直是个容易出错的问题,很多开发者会写出 JSON.stringify(a) === JSON.stringify(b) 这样的代码来比较对象,但这样做有很多缺陷:

  1. 无法处理循环引用 :如果对象中存在循环引用,JSON.stringify 会抛出错误
  2. 忽略 undefined、函数和 Symbol :这些值在序列化过程中会被忽略或转换为 null
  3. 无法区分 MapSetDate 等特殊对象:这些对象会被转换为普通对象或字符串
  4. 属性顺序敏感:如果两个对象属性顺序不同但内容相同,会被判断为不相等
  5. 无法自定义比较逻辑:无法实现类似"年龄相差 5 岁内算匹配"这样的灵活比较
  6. 性能问题:对于大对象,序列化和字符串比较的性能开销很大

而 Lodash 的 baseIsMatch 及相关函数提供了一套全面而高效的解决方案,让对象比较变得简单可靠。

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端