目录

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 及相关函数提供了一套全面而高效的解决方案,让对象比较变得简单可靠。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
—Qeyser4 小时前
用 Deepseek 写的uniapp血型遗传查询工具
前端·javascript·ai·chatgpt·uni-app·deepseek
codingandsleeping4 小时前
HTTP1.0、1.1、2.0 的区别
前端·网络协议·http
小满blue4 小时前
uniapp实现目录树效果,异步加载数据
前端·uni-app
喜樂的CC5 小时前
[react]Next.js之自适应布局和高清屏幕适配解决方案
javascript·react.js·postcss
天天扭码5 小时前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
咖啡虫6 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
烛阴6 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
拉不动的猪6 小时前
设计模式之------策略模式
前端·javascript·面试
旭久6 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc6 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf