likes
comments
collection
share

我一直以为这就是JS中的浅比较,直到...

作者站长头像
站长
· 阅读数 13

写在前面

在我们使用进行React开发过程中,浅比较是一个绕不过去的话题,它在不同的过程中扮演着关键的角色,比如用来判断class组件是否应该更新、比较React hooks的依赖数组、判断React.memo包装的组件是否应该更新等。 那么,什么是浅比较呢?当我在google中搜索“浅比较”,得到的答案是这样的:

我一直以为这就是JS中的浅比较,直到...

和这样的:

我一直以为这就是JS中的浅比较,直到...

是的,我一直以为===就是浅比较,直到有一天我看到了React文档中对于浅比较的定义

我一直以为这就是JS中的浅比较,直到...

翻译一下: 浅比较负责对当前props和新props对象以及当前state和新state对象执行浅层相等性检查。 它通过迭代被比较对象的key并在每个对象中的value不严格相等时返回true来实现。

简单来说,浅比较会对被比较对象的键值(注意不是比较对象自身)判断是否严格相等,而根据 MDN WEB DOCS严格相等运算符就是===

我一直以为这就是JS中的浅比较,直到...

所以说,===和浅比较并不一样!那么到底什么是浅比较呢?它和===又有哪些不同呢?JS中还有哪些比较方式呢?别急,且听我细细道来。

如果你觉得本文对你有所帮助,请不要吝啬你的 赞👍 哦!你的认可是我持续写作的最大动力!谢谢!

全文简介

本文介绍了JavaScript中对对象进行比较的几种方法,主要包括:

  1. 引用比较(包括=====Object.is()
  2. 手动比较
  3. 浅比较
  4. 深比较

对于每种比较方法,都给出了具体实现,并比较了和其他方法的不同。

对于浅比较,本文详细讲解了其在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'};编译器会做如下处理:

  1. 遇到const person1,编译器会询问作用域是否已经有一个该名称的变量存在。如果不存在,它会要求作用域声明一个新的变量(即分配一块新内存空间),并命名为person1
  2. 接下来编译器会为引擎生成运行时所需的代码,用来处理person1 = {name: 'Bob'}这个赋值操作。

同样,对于person2,编译器也会分配新的内存并赋值。person1person2使用的内存不同,当然比较结果是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])

总结

  1. 引用比较(使用=====Object.is())用来确定操作数是否为同一个对象实例。
  2. 手动比较对象是否相等,需要针对属性值进行手动编写比较函数比较,因此这种方法非常灵活。
  3. 当被比较的对象有很多属性或无法提前确定对象的结构时,更好的方法是使用浅比较浅比较===的区别是:浅比较是对数据所有键(索引)对应的值进行严格相等比较(使用===Object.is()),而===是对数据本身进行严格相等比较
  4. 如果比较的对象具有较深的嵌套层次,而我们只需要关注内容的一致性时,则应该进行深比较

如果你觉得本文对你有所帮助,请不要吝啬你的 赞👍 哦!你的认可是我持续写作的最大动力!谢谢!

参考资料