在 JS 中的关系比较 (Relational Comparison) 运算,指的是像 x < y 这种大小值的关系比较。
而相等比较,可区分为标准相等 (standard equality) 比较 x == y 与严格相等 (strict equality) 比较 x === y 两大种类。严格相等比较会比较左边与右边运算元的数据类型,值相等比较则只看值,简单的来说是这样解释没错。
ToPrimitive 运算的详细说明可参考:
不过,这两种比较实际上依内部设计来说,并不是那么简单。当然,在一般的使用情况是不需要考量那么多,本文的说明会涉及许多 JS 内部设计的部份,对于这两种比较来作比较彻底的理解,主要的参考数据是 ECMAScript 的标准文件。
严格相等比较的演算规则先理解,主要是因为在标准相等比较 (只比较值不比较数据类型) 时,它在演算时的某些情况下会跳到严格相等比较的规则来。
严格相等比较的演算规则很容易理解,按照以下的步骤进行比较,出自 :
以下假设为比较 x === y 的情况,Type(x) 指的是 x 的数据类型,Type(y) 指的是 y 的类型,最终返回值只有 true 或 false,会按照下面的步骤进行比较,如果有返回时就停止之后的步骤:
注: Type(x) 在 ECMAScript 的标准中指的并不是用 typeof 返回出来的结果,而是标准内部给定的各种数据类型,共有 Undefined, Null, Boolean, String, Number 与 Object。例如 typeof null 的结果是 "object",但 ECMAScript 会认为 Null 是个独立的数据类型。
备注: 这个演算与 the SameValue Algorithm (9.12) 不同之处在于,对于有号的 0 与 NaN 处理方式不同。
注: 同值演算 (the SameValue Algorithm) 是标准中的另一个内部演算法,只会用在很特别的地方,可以先略过不看。
从上述的严格相等比较中,可以很清楚的看到数字、字符串、布尔与 null、undefined 或对象是如何比较的。
标准相等比较的演算规则按照以下的步骤进行比较,出自 :
以下假设为比较 x == y 的情况,Type(x) 指的是 x 的数据类型,Type(y) 指的是 y 的类型,最终返回值只有 true 或 false,会按照下面的步骤进行比较,如果有返回时就停止之后的步骤:
备注 1: 以下的是三种强制转换的标准比较情况:
备注 2: 标准相等比较有以下的不变式 (invariants):
备注 3: 相等比较运算不一定总是可以转变 (transitive),例如:
备注 4: 字符串比较使用的是简单的字符测试。并非使用复杂的、语义导向的字符定义或是 Unicode 所定义的字符串相等或校对顺序。
注: 上述的 ToNumber 与 ToPrimitive 都是标准内部运算时使用的方法,并不是让开发者使用的。
由标准相等比较的演算得知,它的运算是以 "数字为最优先",任何其它的类型如果与数字作相等比较,必定要先强制转为数字再比较。但这是一个相当具有隐藏作用的运算,在一般实作时,会很容易造成误解,例如以下的例子:
- > 0 == []
- true
- > '' == []
- true
上面这是因为空数组 [] ,进行 ToPrimitive 运算后,得到的是空字符串,所以作值相等比较,相当于空字符串在进行比较。
- > '[object Object]' == {}
- true
- > NaN == {}
- false
上面的空对象字面量,进行 ToPrimitive 运算后,得到的是 '[object Object]' 字符串,这个值会如果与数字类型的 NaN 比较,会跳到同类型相等的严格相等比较中,NaN 不论与任何数字作相等比较,一定是返回 false。
- > 1 == new Number(1)
- true
- > 1 === new Number(1)
- false
- > 1 === Number(1)
- true
上面说明了,包装对象在 JS 中的内部设计中,标准的值相等比较是相同的,但严格相等比较是不同的值,包装对象仍然是个对象,只是里面的 valueOf 方法是返回这个对象里面带的原始数据类型值,经过 ToPrimitive 方法运算后,会返回原始数据的值。 Number() 函数调用只是转数字类型用的函数,这个用法经常会与包装对象的用法混在一起。
这个小节的结论是,在 JS 中没有必要的情况下,使用严格的相等比较为最佳的值相等比较方式,标准的相等容易产生不经意的副作用,有的时候你可能会得到不预期的结果。
关系比较的演算规则主要是按照以下的步骤进行比较,出自 :
以下假设为比较 x <y 的情况,因为在标准中的抽象关系比较演算的说明比较复杂,有涉及布尔标记的以左方优先或右方优先,而且最终返回值有 true、false 与 undefined,实际上最终不会有 undefined 值出现,即是得到 false 而已,以下为只考虑左方优先 (LeftFirst) 的简化过的步骤。会按照下面的步骤进行比较,如果有返回时就停止之后的步骤:
备注 2: 字符串比较使用的是简单的词典顺序测试。并非使用复杂的、语义导向的字符定义或是 Unicode 所定义的字符串相等或校对顺序。
注: +∞ 相当于全局属性 Infinity 或 Number.POSITIVE_INFINITY , −∞ 相当于全局属性 -Infinity 或 Number.NEGATIVE_INFINITY 。
关系比较基本上要区分为数字类型与字符串类型,但依然是以 "数字" 为最优先的比较,只要有其他类型与数字相比较,一定会先被强制转换为数字。但在这之前,需要先用 ToPrimitive 而且是 hint 为数字来转换为原始数据类型。
以下为一些与对象、数组、Date 对象的关系比较例子:
- > 1 < (new Date())
- true
- > 1 > (new Date())
- false
- > [] < 1
- true
- > [] > 1
- false
- > ({}) < 1
- false
- > ({}) > 1
- false
虽然在标准中的抽象关系比较演算中,有存在一种返回值 undefined ,但在真实的情况并没有这种返回值,相当不论怎么比较都是得到 false 的值。上面的例子中,空对象 ({}) 的 ToPrimitive 运算得出的是 '[object Object]' 字符串值,经过 ToNumber 运算会得到 NaN 数字类型的值,这个值不论与数字 1 作大于小于的关系运算,都是 false。
Date() 对象因为 ToPrimitive 运算的 hint 为数字,所以也是会作转换为数字类型的值为优先 (也就是调用 valueOf 为优先),所以并不是正常情况的以输出字符串为优先(也就是调用 toString 方法为优先) 的预设情况。
以下为一些字符串关系比较的例子:
- > 'a' > ''
- true
- > 'a' < ''
- false
- > 'a' > []
- true
- > 'a' < []
- false
- > 'a' > ({})
- true
- > 'a' < ({})
- false
字符串与空字符串相比,都是套用前缀 (prefix) 的规则步骤,因为空字符串算是所有字符串的前缀(组成的子字符串之一),所以必然地所有有值的字符串值一定是大于空字符串。
空数组经过 ToPrimitive 运算出来的是空字符串,所以与空字符串相比较的结果相同。
空对象经过 ToPrimitive 运算出来的是 '[object Object]' 字符串值,以'a'.charCodeAt(0) 计算出的值是字符编码是 97 数字,而 '['.charCodeAt(0) 则是 91 数字,所以'a' > ({}) 会是得到 true。
如果开始混用数字与字符串比较,可能是有陷阱的比较例子:
- > '11' > '3'
- false
- > '11' > 3
- true
- > 'one' < 3
- false
- > 'one' > 3
- false
'11'与'3'相比较,其实都是字符串比较,要依照可比较的字符位置来比较,也就是'1'与'3'字符的比较,它们的字符编码数字分别是 49 与 51,所以 '1' <'3' ,这里的运算的结果必然是返回 false。
'11'与 3 数字比较,是会强制都转为数字来比较,'11'会转为 11 数字值,所以大于 3。
'one'这个字符串转为数字后,是 NaN 这个数字值,NaN 与任何数字比较,既不大于也不小于,不论作大于或小于,都是返回 false。(实际上在标准中它这种返回值叫 undefined)
字符串与数字之外其他的原始数据类型的比较,只要记得原则就是强制转为数字来比较就是了,以下为例子:
- > true > null
- true
- > false > undefined
- false
简单地说明在 ToNumber 运算时,这些其他的原始数据类型值的转换结果如下:
注: JS 认为 + 0 与 - 0 是完全相同的值,在严格相等比较中是相等的。
注: 字符串比较实际上是拆为字符在词典表中的编辑整数值来比较,对于非英语系的语言,JS 另外有提供 的方法来进行局部语言的比较工作。
本章延伸了之前的加法运算文章中的 ToPrimitive 运算解说的部份,较为仔细的来研究 JS 中的相等比较 (包含标准的与严格的) 与关系比较的部份。至于没提到的,不相等 (==) 与严格不相等 (!==),或是大于等于(>=) 或小于等于 (<=) 只是这些演算规划的再组合结果而已。
标准的值相等比较 (==),是一种有不经意的副作用的运算,不管如何,开发者必定要尽量避免,比较前可以自行转换类型的方式,再作严格的相等比较,本章也有说明为何要避免使用它的理由。
来源: