当你在javascript中用"=="的时候, 你在比较什么?

类型转换

  js中存在强制类型转换和隐式类型转换, 有意识地去调用转换方法或者构造函数去转换称为强制类型转换(implicit coercion), 无意识地转换(语言机制自发完成)称为隐式类型转换(explicit coercion).

1
2
3
const a = 42
const b = a + "" // implicit coercion
const c = String( a ) // explicit coercion

这里稍后会讨论的”==”问题, 涉及到的也是隐式类型转换.
转换的目标只能是string, number or boolean. 不可能经过隐式类型转换, 转换出一个复杂类型的数据(Object, Array, Function …). 现在来看看各个类型的ToString,ToNumber, ToBoolean, 或者说是ToPrimitive.

ToString

  • 原生简单对象转化规则
    null: 转化为字符串”null”;
    undefined: 转化为”undefined”;
    true/false: 转化为”true”/“false”;
    Number: 大部分情况如预测地那样, 2转化为”2”, 0转化为”0”, 100转化为”100”. 但是事情没有这么简单. 不是10进制的数字, 首先会转化为十进制, 然后再转化为字符串, 并不是数字直接加上引号就行了.

    1
    2
    (0x23).toString() // "35"
    0x23 == "35" // true

    绝对值很大的数值或者绝对值很小的数值, 首先会转化为科学计数法, 然后再进行转化.

    1
    2
    3
    (0.0000001).toString() // "1e-7"
    (1000000000000000000000).toString() // "1e+21"
    0.0000001 == "1e-7" // true
  • 复杂对象的转化规则
    Object: 如果没有指定自己的toString()方法, 就会调用Object.prototype.toString(). 这个函数会返回对象类型字符串, 在这里是”[object Object]”. 如果指定了自己的toString()函数, 会执行这个函数, 使用返回值.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const obj = {
    value: 42,
    }
    const obj42 = {
    value: 42,
    toString: () => {return "ultimate"}
    }
    obj == "[object Object]" // true
    obj42 == "ultimate" // true

    Array: Array类型”重载”了Object.prototype.toString(), toString方法返回有一个以”,”隔开的数组元素拼接的字符串.

    1
    2
    3
    4
    5
    6
    7
    const arr = ['a','b','c','d','e','f']
    console.log(arr) // "a,b,c,d,e,f"
    // 修改数组的默认toString方法 别这样做
    Array.prototype.toString = function() {
    return this.split('-')
    }
    ['a','b','c','d','e','f'] == "a-b-c-d-e-f" // true

    Function: Function类型也重载了Object.prototype.toString(), 个性化的toString返回函数的字符串形式.

    1
    (function(){var s = 2;return s}) == "function(){var s = 2;return s}"

ToNumber

  • 基本类型转化规则
    null: 转化为0!;
    undefined: 转化为NaN;
    true/false: 转化为1/0;
    String: 字符串会尝试使用Number()构造函数(误)去转化结果, 转化失败不会报错, 会返回特殊的数字类型值NaN. 在这种操作中可以正确辨识以0x(0X)为起始符号的16进制的数字字符串, 但是会忽略以”0”起始的部分.

    1
    2
    3
    4
    5
    6
    10 - null // 10
    isNaN(undefined) // true
    1 + true // 2
    2 - false // 2
    20 - "0xb" // 9
    20 - "013" // 7

    还有一种特殊情况是, 如果是合法的科学计数法数字字符串, 能正常转化为10进制的数字

    1
    "1e+10" == 10000000000 // true
  • 复杂对象的转化规则
    首先复杂对象会调用内部的ToPrimitive方法, 尝试转化成基础类型值, 如果基础类型值不是number, 则再进行转化. 调用ToPrimitive可以想成首先尝试调用对象的valueOf()方法, 如果有这个方法并且返回的是基础类型值则使用返回值, 否则就尝试调用toString()方法. 如果这两个方法都不存在或者返回值都不是基础类型值, 会抛出TypeError错误.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const a = {
    valueOf: () => {return "1"}
    }
    10 - a // 9
    const b = {
    toString: () => {return "2"}
    }
    10 - b // 8
    const c = {
    valueOf: () => {return "3"}, // 首先调用
    toString: () => {return "4"},
    }
    10 - c // 7
    const d = {
    valueOf: () => {return {}}, // 首先调用
    toString: () => {return "5"}, // 调用valueOf结果不对, 调用toString
    }
    10 - d // 5
    const e = Object.create(null)
    10 - e // TypeError

ToBoolean

先来看一定是false的几个值

  • undefined
  • null
  • false
  • +0, -0, and NaN
  • “”

    这个列表外的值, 都是true(误, 有例外). 复杂类型的值都是true(误, 有例外)

    1
    2
    !!new Boolean( false ) // true
    !!new Boolean( 0 ) // true

    在史前时代, 人们判断是不是IE浏览器, 往往用这样的代码:

    1
    if (document.all) { /* it's IE */ }

    结果慢慢地, 别的浏览器也开始有这个API了. 可是旧代码已经沉淀下来成了地层中的岩石, 挖出来修改的成本太高了, 干脆在非IE浏览器中document.all是falsy算了, 所以导致了这个对象的奇葩行为.

    1
    2
    !!document.all // true 在IE11以下版本
    !!document.all // false 在IE11以上版本或非IE环境

“==” VS. “===”

两者的区别是: “==”比较的时候, 允许隐式类型转换, “===”不允许隐式类型转换.
稍微提下”===”的两个奇葩行为:

  • NaN === NaN // false
  • -0 === +0 // true

“==”规则

1
2
42 === '42' // false
42 == '42' // true

问题来了, 42 == ‘42’到底隐式转换成了什么? 是42 == 42还是’42’==’42’? 接下来就详细介绍下转换的规则, 了解这些规则后, “==”很多诡异的行为都变得有理有据, 再也不用视为”糟粕”不敢用了.

String VS. Number

If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.

如果”==”两边是字符串和数字, 那么字符串转化为数字去比较.
字符串转化为数字的规则, 上边有介绍.

Anything VS. Boolean

If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).

如果”==”两边是Boolean值和其他值, 那么第一步会将Boolean值转化为数字, 转化的结果只能是0或1. 然后再用0或1去和其他值比较, 如果其他值是复杂类型的值, 再进行其他转换, 如果是字符串, 参考上一条.

1
2
true == '1' // 1 == '1' -> 1 == 1 true
true == '42' // 1 == '42' -> 1 == 42 false

null VS. undefined

If x is null and y is undefined, return true.
If x is undefined and y is null, return true.

如果是null和undefined作比较, 返回true. 这俩哥们和其他的任何值作比较, 都返回false.

1
2
3
4
5
6
7
8
9
10
11
const a = null
const b = undefined
a == b // true
a == null // true
b == null // true
a == false // false
b == false // false
a == "" // false
b == "" // false
a == 0 // false
b == 0 // false

Objects VS. non-Objects

If Type(x) is either String or Number and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
If Type(x) is Object and Type(y) is either String or Number, return the result of the comparison ToPrimitive(x) == y.

当复杂类型与基本类型作比较的时候, 复杂类型值首先要转换成基本类型的值, 转化规则前边有介绍.

1
2
['42'] == 42 // true
Object(10) == '10' // true

两点需要注意, 构造函数的参数是null或者undefined, 会返回一个”空”对象, 所以下边的结果是有道理的.

1
2
3
4
const a = Object( null ) // {}
a == null // false
const b = Object( undefined ) // {}
b == undefined // false

最后练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[] == ![] // true [] == false -> [] == 0 -> "" == 0 -> 0 == 0

2 == [2] // true 2 == "2" -> 2 == 2

"" == [null] // true "" == ""  (ps: String([null]) === "";  String(null) === "null")

"0" == false // true "0" == 0 -> 0 == 0

false == 0 // true 0 == 0

false == "" // true 0 == "" -> 0 == 0

false == [] // true 0 == [] -> 0 == "" -> 0 == 0

"" == 0 // true 0 == 0

"" == [] // true "" == ""

0 == [] // true 0 == "" -> 0 == 0