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
的实现很聪明,分成三个主要步骤:
-
先看有没有东西比 :首先检查目标对象是不是
null
或undefined
,如果是空的,那就只有在没有属性需要匹配时才算匹配成功。 -
快速初筛 :第一轮循环做快速检查,对那些可以用
===
直接比较的属性(比如数字、字符串),直接用不等号比;对于复杂类型,就先看属性存不存在。只要发现一个不匹配,立刻返回false
。 -
深入比较:第二轮循环做详细比较,尤其是对象和数组这类复杂类型:
- 对于简单类型,再检查一下
undefined
的特殊情况 - 对于复杂类型,要么用自定义比较器,要么用
baseIsEqual
进行深度递归比较
- 对于简单类型,再检查一下
这种设计既保证了比较结果的准确性,又通过"先简单后复杂"的策略大大提高了性能。
源码解析
函数入参详解
baseIsMatch
函数接收四个参数:
-
object: 要检查的目标对象
- 这是我们要分析的主体,判断它是否包含源对象的所有属性和值
- 可以是任何类型,包括基本类型、对象、数组等
- 如果是
null
或undefined
,会有特殊处理逻辑
-
source: 提供匹配属性的源对象
- 包含了我们想要在目标对象中找到的属性和值
- 实际比较中并不直接使用,而是通过
matchData
间接使用 - 主要用于自定义比较器的调用
-
matchData: 匹配数据数组
- 由
getMatchData(source)
预处理生成 - 是一个特殊格式的数组,每个元素也是一个数组,形如
[key, value, isStrictComparable]
key
: 属性名value
: 属性值isStrictComparable
: 布尔值,表示该值是否可用===
严格比较
- 预处理源对象可以提高比较效率,避免重复判断值的类型
- 由
-
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);
这段代码先初始化几个变量:
index
和length
都设为matchData
的长度,一个用来遍历,一个保存原始长度noCustomizer
用来标记是否没有自定义比较器,等于!customizer
然后检查 object
是不是 null
或 undefined
:
- 如果是空值,只有在
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
数组,进行深度比较:
-
对于可以简单比较的值(
noCustomizer && data[2]
为真):- 主要处理
undefined
的特殊情况 - 检查
objValue === undefined && !(key in object)
,区分"属性不存在"和"属性值为 undefined"的情况
- 主要处理
-
对于需要深度比较的值或有自定义比较器的情况:
- 创建一个新的
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
虽然是个内部函数,但它的设计思想非常值得学习:
-
性能优化:
- 两次循环分别处理简单和复杂情况,简单的先检查,能快速失败就快速失败
- 用短路求值避免不必要的深度比较
- 对可以用
===
比较的值进行特殊处理,避免创建不必要的Stack
实例
-
灵活性:
- 支持自定义比较器,让用户能定义自己的比较逻辑
- 特殊处理各种边界情况,如
null
、undefined
值 - 支持深度嵌套对象的比较
-
正确性:
- 使用
Stack
处理循环引用问题,防止无限递归 - 区分"属性不存在"和"属性为 undefined"的情况
- 配合
baseIsEqual
支持复杂数据结构的精确比较
- 使用
实际上,JavaScript 的对象比较一直是个容易出错的问题,很多开发者会写出 JSON.stringify(a) === JSON.stringify(b)
这样的代码来比较对象,但这样做有很多缺陷:
- 无法处理循环引用 :如果对象中存在循环引用,
JSON.stringify
会抛出错误 - 忽略
undefined
、函数和 Symbol :这些值在序列化过程中会被忽略或转换为null
- 无法区分
Map
、Set
、Date
等特殊对象:这些对象会被转换为普通对象或字符串 - 属性顺序敏感:如果两个对象属性顺序不同但内容相同,会被判断为不相等
- 无法自定义比较逻辑:无法实现类似"年龄相差 5 岁内算匹配"这样的灵活比较
- 性能问题:对于大对象,序列化和字符串比较的性能开销很大
而 Lodash 的 baseIsMatch
及相关函数提供了一套全面而高效的解决方案,让对象比较变得简单可靠。