我一直以为这就是JS中的浅比较,直到...
写在前面
在我们使用进行React开发过程中,浅比较是一个绕不过去的话题,它在不同的过程中扮演着关键的角色,比如用来判断class组件是否应该更新、比较React hooks的依赖数组、判断React.memo包装的组件是否应该更新等。 那么,什么是浅比较呢?当我在google中搜索“浅比较”,得到的答案是这样的:
和这样的:
是的,我一直以为===
就是浅比较,直到有一天我看到了React文档中对于浅比较的定义:
翻译一下: 浅比较负责对当前props和新props对象以及当前state和新state对象执行浅层相等性检查。 它通过迭代被比较对象的key并在每个对象中的value不严格相等时返回true来实现。
简单来说,浅比较会对被比较对象的键值(注意不是比较对象自身)判断是否严格相等,而根据 MDN WEB DOCS严格相等运算符就是===
:
所以说,===
和浅比较并不一样!那么到底什么是浅比较呢?它和===
又有哪些不同呢?JS中还有哪些比较方式呢?别急,且听我细细道来。
如果你觉得本文对你有所帮助,请不要吝啬你的 赞👍 哦!你的认可是我持续写作的最大动力!谢谢!
全文简介
本文介绍了JavaScript中对对象进行比较的几种方法,主要包括:
- 引用比较(包括
===
、==
、Object.is()
) - 手动比较
- 浅比较
- 深比较
对于每种比较方法,都给出了具体实现,并比较了和其他方法的不同。
对于浅比较,本文详细讲解了其在React中的实现(3.1-3.5节),将其与===
的使用进行了对比(3.6节),并结合React.memo包装下子组件渲染的例子讲解了其应用(3.7节)。
主要内容
1、引用比较
上文说到,JavaScript中提供三种不同的值比较操作:
- 严格相等比较 (也被称作"strict equality", "identity", "triple equals"),使用
===
- 抽象相等比较 (也被称作"loose equality", "double equals") ,使用
==
Object.is
(ECMAScript 2015/ ES6 新特性)
当使用上述任何方法比较对象时,仅当对象的引用相同,结果才为true。这就是引用比较。
1.1 Let's try it!
举个栗子,定义对象 person1
和 person2
,让我们看一下实际的引用相等性:
const person1 = {
name: 'Bob'
};
const person2 = {
name: 'Bob'
};
// 使用===比较
console.log(person1 === person1); // => true
console.log(person1 === person2); // => false
// 使用==比较
console.log(person1 == person1); // => true
console.log(person1 == person2); // => false
// 使用Object.is比较
console.log(Object.is(person1, person1)); // => true
console.log(Object.is(person1, person2)); // => false
为什么会这样呢?对于const person1 = {name: 'Bob'};
编译器会做如下处理:
- 遇到
const person1
,编译器会询问作用域是否已经有一个该名称的变量存在。如果不存在,它会要求作用域声明一个新的变量(即分配一块新内存空间),并命名为person1
。 - 接下来编译器会为引擎生成运行时所需的代码,用来处理
person1 = {name: 'Bob'}
这个赋值操作。
同样,对于person2
,编译器也会分配新的内存并赋值。person1
和person2
使用的内存不同,当然比较结果是false啦,即使他们的值是相同的,都是{name: 'Bob'}
。
当你想比较对象的引用而不是它们的内容时,引用相等是很有用的。但是在更多的情况之下,你都想针对对象的实际内容进行比较:例如属性及它们的值。
接下来看看如何通过对象的内容比较对象是否相等。
2、手动比较
按内容比较Object最直接的方法是读取属性并手动比较它们。
2.1 Let's try it!
例如,我们编写一个特殊的函数 isPersonEqual()
来比较两个 perosn 对象:
const person1 = {
name: 'Bob'
};
const person2 = {
name: 'Bob'
};
const person3 = {
name: 'Tom'
};
const isPersonEqual = (object1, object2) => {
return object1.name === object2.name;
};
console.log(isPersonEqual(person1, person2)); // => true
console.log(isPersonEqual(person1, person3)); // => false
手动比较的手动就体现在我们可以根据需要手动灵活定义比较函数isPersonEqual(object1, object2)
。这类函数具有良好的性能:在比较中只需要涉及少数几个我们关心的Object属性和相等运算符。
手动比较需要手动提取属性,对于简单对象来说,这不是问题。但是,如果要对较大的对象(或结构未知的对象)进行比较,就不方便了。这时候我们就需要浅比较。
3、浅比较
什么是浅比较?
两个对象(数组)进行浅比较,就是对其所有键(索引)对应的值进行严格相等比较(使用===
或Object.is()
)。
如果所有键(索引)对应的值都严格相等,那么这两个对象(数组)就是浅比较相等的。否则不等。
最直接了解浅比较的方式就是去深入它的实现。React中实现浅比较相应的代码可以在React Github项目的shared包中的 shallowEqual.js找到。代码如下:
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
//----第1部分----
if (is(objA, objB)) {
return true;
}
//----第2部分----
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
//----第3部分----
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
//----第4部分----
// Test for A`s keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
函数接收两个入参作为被比较的对象。这个代码使用了Flow作为类型检测系统而不是使用TypeScript。两个函数的参数都使用了Flow中的mixed类型(类似TypeScript中的unknown)。这表明它们可以是任意类型。
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
}
接下来我们由上到下分4部分对shallowEqual函数进行解析。
3.1 shallowEqual第1部分
首先使用React的内部实现的is()
方法对两个函数参数进行比较,这个借助于===
实现的is()
内部方法实际上是Object.is()
的polyfill。那为啥不直接使用===
呢?
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
// ...
}
Object.is()
和===
虽然基本相同,但是有两个例外:
- Object.is将+0和-0当作不相等,而===把他们当作相等
- Object.is把 Number.NaN和Number.NaN当作相等,而===把他们当作不相等
所以,上面的is()
方法就是对+0,-0,Number.NaN进行了特殊处理。
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: mixed, y: mixed): boolean {
// SameValue algorithm
if (x === y) { // Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
// Added the nonzero y check to make Flow happy, but it is redundant
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
}
总结一下,第1个部分能处理如下简单的情况:如果两个参数objA和objB有相同的值(基本类型值相等,引用类型引用相等),则它们会被认为相等。
3.2 shallowEqual第2部分
为了确保接下来第3部分是比较两个复杂的数据结构,第2部分还需要检查是否其中一个参数不是对象或者是null
。前一个检查确保我们处理的两个参数是对象或数组,而后一个检查是过滤掉null
,因为typeof null === 'object'
。
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// ...
}
3.3 shallowEqual第3部分
现在可以确定我们只处理数组和对象的比较了。首先,我们简单比较它们的键的数量是否相等。如果键的数量不相等,直接返回false,不用进入第4部分,这样可以提高算法的效率。
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// ...
}
3.4 shallowEqual第4部分
最后,就是shallowEqual的核心部分了。我们根据key遍历两个objA和objB并逐个比较value是否相等。基于上一步中生成的键数组keysA
,使用hasOwnProperty
检查key是否实际上是对象自身的属性,并使用is()
函数进行比较(判断引用是否相等)。
只要一个value的引用不相等,那么shallowEqual就返回false。全部相等,返回true。
const hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
3.5 Let's try it!
让我们用shallowEqual()
来看看实际结果如何吧:
我们用prevDeps1、nextDeps1、prevDeps2和nextDeps2来模拟hooks的依赖数组。 当对数组进行浅比较时,实际上是对其每个索引进行严格相等比较。
-
基本类型company1和company2相等,因此prevDeps1、nextDeps1浅比较结果为true;
-
对象person1和对象person2引用不相等,因此prevDeps2、nextDeps2浅比较结果为false。
我们用prevProps1、nextProps1、prevProps2和nextProps2来模拟组件的props。 同样,当对对象进行浅比较时,实际上是对其每个键值进行严格相等比较。
const person1 = {
name: 'Bob'
};
const company1 = 'Google';
const person2 = {
name: 'Bob'
};
const company2 = 'Google';
/** 模拟hooks的依赖数组 **/
const prevDeps1 = [person1, company1];
const nextDeps1 = [person1, company2];
console.log(shallowEqual(prevDeps1, nextDeps1)); // => true
const prevDeps2 = [person1, company1];
const nextDeps2 = [person2, company2];
console.log(shallowEqual(prevDeps2, nextDeps2)); // => false
/** 模拟组件的props **/
const prevProps1 = {
person: person1,
company: company1
};
const nextProps1 = {
person: person1,
company: company2
};
console.log(shallowEqual(prevProps1, nextProps1)); // => true
const prevProps2 = {
person: person1,
company: company1
};
const nextProps2 = {
person: person2,
company: company2
};
console.log(shallowEqual(prevProps2, nextProps2)); // => false
3.6 浅比较 VS ===
浅比较与===
的区别是:浅比较是对数据所有键(索引)对应的值进行严格相等比较(使用===
或Object.is()
),而===
是对数据本身进行严格相等比较。
以下面的数组比较为例:
- arr1与arr2是不同的变量,引用不同,严格相等比较结果自然为false。
- 当arr1与arr2进行浅比较时,person1与person1是同一变量,严格比较结果为true;company1与company2都为基本数据类型,值相同,严格比较结果也为true,所以arr1与arr2浅比较结果为true。
const person1 = {
name: 'Bob'
};
const company1 = 'Google';
const person2 = {
name: 'Bob'
};
const company2 = 'Google';
/** 数组比较 **/
const arr1 = [person1, company1];
const arr2 = [person1, company2];
console.log(shallowEqual(arr1, arr2)); // => true
console.log(arr1 === arr2); // => false
/** 对象比较 **/
const obj1 = {
person: person1,
company: company1
};
const obj2 = {
person: person1,
company: company2
};
console.log(shallowEqual(obj1, obj2)); // => true
console.log(obj1 === obj2); // => false
3.7 Let's try it in React!
ok,咱们讨论的浅比较问题缘起于React,最终也要回到React中去实际验证一番:
在下面的demo中,我们用React.memo包装了的子组件。 点击按钮后,state变化,触发重渲染,这时经过React.memo包装的组件会对props进行浅比较。如果props发生变化,则子组件也会重新渲染。
子组件一prevProps为{value: "子组件一"}
,nextProps为{value: "子组件一"}
,shallowEqual结果为true,因此不渲染。
子组件二prevProps为{value: {data: "子组件二"}}
,nextProps为{value: {data: "子组件二"}}
,shallowEqual结果为false,因此重渲染。
3.8 弊端
JS中的对象是可以嵌套的(有时层次会非常深)。如果在这种情况下我们需要比较两个对象的内容,浅比较就不能很好地发挥作用了。
const prevProps2 = {
person: {
name: 'Bob'
},
company: 'Google'
};
const nextProps2 = {
person: {
name: 'Bob'
},
company: 'Google'
};
console.log(shallowEqual(prevProps2, nextProps2)); // => false
由于嵌套对象 prevProps2.person 和 nextProps2.person 是不同的对象实例,引用不同。因此,即使prevProps2和nextProps2具有相同的内容,shallowEqual(prevProps2, nextProps2)
也将会返回false。
进行嵌套对象内容比较时需要用到深比较。
4、深比较
以下是深比较的一种实现
const isObject = object => {
return object != null && typeof object === 'object';
};
const deepEqual = (objA, objB) => {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (let i = 0; i < keysA.length; i++) {
const val1 = objA[keysA[i]];
const val2 = objB[keysB[i]];
const areObjects = isObject(val1) && isObject(val2);
if (areObjects && !deepEqual(val1, val2)) {
return false;
}
if (!areObjects && val1 !== val2) {
return false;
}
}
return true;
}
深比较与浅比较相似,不同之处在于,当属性中包含对象时,将会对嵌套对象执行递归浅层比较。
4.1 Let's try it!
const prevProps2 = {
person: {
name: 'Bob'
},
company: 'Google'
};
const nextProps2 = {
person: {
name: 'Bob'
},
company: 'Google'
};
console.log(shallowEqual(prevProps2, nextProps2)); // => true
当然,在实际开发过程中,我们不用重复造轮子,需要深比较方法时可以直接调用如Node内置util模块的 isDeepStrictEqual(object1, object2)
或lodash库的_.isEqual(value, other)
、_.isEqualWith(value, other, [customizer])
。
总结
- 引用比较(使用
===
、==
或Object.is()
)用来确定操作数是否为同一个对象实例。 - 手动比较对象是否相等,需要针对属性值进行手动编写比较函数比较,因此这种方法非常灵活。
- 当被比较的对象有很多属性或无法提前确定对象的结构时,更好的方法是使用浅比较。
浅比较与
===
的区别是:浅比较是对数据所有键(索引)对应的值进行严格相等比较(使用===
或Object.is()
),而===
是对数据本身进行严格相等比较。 - 如果比较的对象具有较深的嵌套层次,而我们只需要关注内容的一致性时,则应该进行深比较。
如果你觉得本文对你有所帮助,请不要吝啬你的 赞👍 哦!你的认可是我持续写作的最大动力!谢谢!
参考资料
转载自:https://juejin.cn/post/7170364934889406495