Skip to content

比较算法

在JavaScript中,有这些场景涉及到数据的比较:

  • a == b
  • a === b [1, 2, 3].indexOf(1)
  • Object.is(a, b)
  • Set/Map 等集合中内部判断元素唯一性

在比较时,主要的差异就是数值的处理,除了数值类型,其他类型的比较比较简单:

  • undefined = undefined
  • null = null
  • true = true false = false
  • 'abc' = 'abc'
  • Symbol、对象判断引用地址是否相等

而数值由于有很多边界值,因此要考虑的问题更多:

  • NaN 等于 NaN 吗?
  • +0 等于 -0 吗?
  • Infinity 等于 Infinity 吗?

除了数值类型比较的差异,非数值类型的比较算法得到了统一,这个算法就是 SameValueNonNumber

各场景下所使用的算法及细节如下表所示:

场景算法说明数值比较算法非数值比较算法
a === bIsStrictlyEqual严格相等Number::equalSameValueNonNumber
a == bIsLooselyEqual宽松相等//
Object.is(a, b)SameValue同值Number::sameValueSameValueNonNumber
new Set([1, 1])SameValueZero同值(包括零)Number::sameValueZeroSameValueNonNumber

四个底层算法

接下来,我们先学习SameValueNonNumber、Number::equal、Number::sameValue、Number::sameValueZero这四个底层比较算法。

SameValueNonNumber(x, y)

  • 比较细节
    • x、y必须同一类型
    • x、y同为 undefinednull ,返回 true
    • x、y为boolean,则同为 truefalse ,返回 true
    • x、y为bigint,则使用Bigint的 equal 算法
    • x、y为字符串,同样长度且每个索引位置的字符一样,返回 true
    • 其他类型则直接判断指针地址,相同,返回 true

Number::equal

  • 比较细节:
    • NaN 不等于 NaN
    • -0+0 相等
    • 其他数值按照是否相等判断
  • 相等算法将 -0+0 视为相等,但不同的 NaN 视为不相等

Number::sameValue

  • 比较细节:
    • NaN等于 NaN
    • -0+0 互不相等
    • 其他数值按照是否相等判断
  • 同值算法将不同的 NaN 值视为同一个值,不包含不同情况的零值

Number::sameValueZero

  • 比较细节:
    • NaN 等于 NaN
    • -0+0 相等
    • 其他数值按照是否相等判断
  • 同值(含零)算法不仅将不同的 NaN 视为同一个值,并且将不同的零值视为同一个值

Number::equal 、Number::sameValue 、Number::sameValueZero 三个算法的差别在于对 NaN-0+0 的不同处理。

不同比较算法细节

SameValue(x, y)

  • 如果类型不同,直接返回 false
  • 如果x为数值,进行 Number::sameValue 比较
  • 如果不为数值,则进行 SameValueNonNumber 比较

SameValueZero(x, y)

  • 如果x、y类型不同,返回 false
  • 如果x为数值,进行 Number::sameValueZero 比较
  • 如果不为数值,则进行 SameValueNonNumber 比较

IsStrictlyEqual(x, y)

  • 如果x、y类型不同,返回 false
  • 如果x为数值,进行 Number::equal 比较
  • 如果不为数值,进行 SameValueNonNumber 比较

IsLooselyEqual(x, y)

  • 比较细节:
    • 如果x、y类型相同,进行 IsStrictlyEqual 比较
    • 如果x、y分别为 undefinednull ,相等
    • x、y一个为数值,一个为字符串时,将字符串进行 ToNumber 操作转成数值,然后重新进行 IsLooselyEqual 比较
    • x、y一个为bigint类型,一个为字符串时,将字符串转成bigint,然后重新进行 IsLooselyEqual 比较
    • x、y一个为布尔值,一个为字符串时,将布尔值转成数值,然后重新进行 IsLooselyEqual 比较
    • x、y中有一个为对象时,先执行 ToPrimitive 操作转基本数据类型,然后重新进行 IsLooselyEqual 比较
    • x、y一个为数值,一个为bigint时,两个值不为无穷且相等时,则x等于y
  • 练习题:
    • '1' == 1 // true
    • true == 1 // true
    • false == 0 // true
    • 'NaN' == NaN // false
    • NaN == true // false
    • NaN == false // false
    • '0' == false // true
  • 宽松相等比较算法的核心是将非数值类型转为数值,再进行严格相等对比

经验

  • NaN 是JavaScript中唯一的自身不等于自身的值
  • 宽松比较时,如果一个值为undefined或null,另一个只要不为undefined或null,则比较结果一定为 false

练习

javascript
const pairs = [
  [undefined, undefined],
  [null, null],
  [true, true],
  [true, false],
  [false, false],
  [1, 1],
  [NaN, NaN],
  [Infinity, NaN],
  [-0, +0],
  [Symbol.iterator, Symbol.iterator],
  [Symbol(), Symbol()],
  ['', ' '],
  ['1', '1'],
  [{ valueOf() { return 1 } }, { valueOf() { return 1 } }],
  [[1], [1]],

  // undefined
  [undefined, null],
  [undefined, true],
  [undefined, false],
  [undefined, 1],
  [undefined, 0],
  [undefined, -0],
  [undefined, +0],
  [undefined, NaN],
  [undefined, Infinity],

  [undefined, Symbol.iterator],
  [undefined, ''],
  [undefined, ' '],
  [undefined, '1'],
  [undefined, 'false'],

  [undefined, {}],
  [undefined, { toString() { return undefined } }],
  [undefined, { valueOf() { return 1 } }],

  // null
  [null, true],
  [null, false],
  [null, 1],
  [null, 0],
  [null, -0],
  [null, +0],
  [null, NaN],
  [null, Infinity],

  [null, Symbol.iterator],
  [null, ''],
  [null, ' '],
  [null, '1'],
  [null, 'false'],

  [null, {}],
  [null, { toString() { return null } }],
  [null, { valueOf() { return 1 } }],

  // boolean
  [true, 1],
  [true, 0],
  [true, 2],
  [true, NaN],
  [false, NaN],
  [true, Infinity],
  [true, -0],
  [true, +0],
  [false, -0],
  [false, +0],
  [true, Symbol.iterator],
  [true, ''],
  [false, ''],
  [true, ' '],
  [false, ' '],
  [true, '0'],
  [true, '1'],
  [false, '0'],
  [false, '1'],
  [true, {}],
  [true, { toString() { return true } }],
  [true, { toString() { return 1 } }],
  [true, []],
  [false, []],
  [true, [1]],
  [false, [1]],
  [true, [0]],
  [false, [0]],
  [true, [0, 1]],
  [false, [0, 1]],

  // number
  [1, '1'],
  [0, '0'],
  [0, '1'],
  [-0, '-0'],
  [+0, '+0'],
  [NaN, 'NaN'],
  [NaN, '1'],
  [Infinity, 'Infinity'],
  [-Infinity, '-Infinity'],
  [0, ''],
  [0, ' '],
  [1, { valueOf() { return 1 } }],
  [1, { valueOf() { return '1' } }],
  [1, { toString() { return 1 } }],
  [1, { toString() { return '1' } }],
  [1, [0]],
  [1, [1]],
  [1, [1, 2]],

  // symbol
  [Symbol.iterator, undefined],
  [Symbol.iterator, null],
  [Symbol.iterator, true],
  [Symbol.iterator, false],
  [Symbol.iterator, 0],
  [Symbol.iterator, -0],
  [Symbol.iterator, +0],
  [Symbol.iterator, NaN],
  [Symbol.iterator, Infinity],
  [Symbol.iterator, -Infinity],
  [Symbol.iterator, ''],
  [Symbol.iterator, ' '],
  [Symbol.iterator, { toString() { return Symbol.iterator } }], // true

  // string
  ['undefined', undefined],
  ['null', null],
  ['', false],
  ['  ', false],
  ['', 0],
  ['', -0],
  ['', +0],
  ['', NaN],
  ['', Infinity],
  ['  ', 0],
  ['  ', -0],
  ['  ', +0],
  ['  ', NaN],
  ['  ', Infinity],
  ['1', 1],
  [' 1 ', 1],
  ['NaN', NaN],
  ['', {}],
  ['', []],
  ['  ', {}],
  ['  ', []], // false [] -> '' / '' != '  '
  ['1', [1]],
  ['1', [1, 2]],
  ['1', { valueOf() { return 1 } }],
  ['1', { toString() { return 1 } }],
  ['1', { valueOf() { return 1 }, toString() { return 2 } }],

  // object
  [{}, {}],
  [{ valueOf() { return 1 } }, { toString() { return 1 } }],
  [{ valueOf() { return '1' } }, [1]],
  [[1], [1]],
  [{ valueOf() { return undefined } }, undefined],
  [{ valueOf() { return null } }, null],
  [{ valueOf() { return 1 } }, '1'],
  [{ toString() { return '1' } }, true],
  [[1], undefined],
  [[0], undefined],
  [[0], null],
  [[0], false],
  [[0], 0],
  [[0], -0],
  [[0], +0],
  [[NaN], NaN],
  [[Infinity], Infinity],
]