如何让 x == 1 && x == 2 && x == 3 等式成立

“ 之前在看一篇文章时,忽然看到一篇有趣的文章,如何让x==1 && x==2 && x==3成立,话不多说,我们直接进入正文吧。”

1. ==和===区别

==操作符:表示宽松相等,它会在比较相等之前先将两边的值进行强制类型转换
===操作符:表示严格相等,它不会执行类型转换,当两个值不是相同类型时,会返回false。
总结:==操作符只要求比较两个值是否相等,而===操作符不仅要求值相等而且还要要求类型相同。

2. ==具体解析

在大致介绍完==和===区别之后,我们再来回顾题目发现它使用到的是宽松相等操作符。先看看下面代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 类型相同时,只需要比较两边值是否相等即可
666 == 666 // true
'666' == '666' // true
false == false // true
// 当然也有例外
NaN == NaN // false
+0 == -0 // true
{} == {} // false 对象是引用类型,两个对象分别指向不同引用,地址不相同

// 类型不相同时
// 1. 字符串和数字
// 首先会把字符串转化为数字,Number('666') == 666
'666' == 666 // true
// 2. 布尔值和其他类型
// 如果布尔值和其他类型比较,会先把布尔值转成数字类型,true == 1
'666' == true // false 所以在写if判断时要注意这种错误写法
// 3. undefined和null
// null和undefined互相相等 | 与其自身相等
null == undefined // true
null == false // false
undefined == 0 // false

上面代码介绍的都是基础类型的一些比较,还有一个特殊的比较:对象类型和其他类型比较;这样比较都会把对象类型转化成基本类型,转化成基本类型又分为2种:数字类型和字符串类型。其实在我之前的一篇文章中介绍了转换的规则,在这篇文章中我就不再讲述了,有兴趣的小伙伴可以看看我这篇文章。
对象转基本类型

3. 题目解析

对象转换成数字类型步骤:
如果是原始值(即一些代表原始数据类型的值,也叫基本数据类型),直接返回这个值;
如果是对象,调用valueOf(),如果得到结果是原始值,返回结果;
如果第二点不满足,则调用toString(),如果得到结果是原始值,返回结果;
如果都不满足,抛出错误

对象转换成字符串类型步骤:
如果是原始值(即一些代表原始数据类型的值,也叫基本数据类型),直接返回这个值;
如果是对象,则调用toString(),如果得到结果是原始值,返回结果;
如果第二点不满足,调用valueOf(),如果得到结果是原始值,返回结果;
如果都不满足,抛出错误

其实心细的你早已经发现了,转换类型是number或者string类型这两个的规则就是将第二点和第三点进行了一个翻转

注意:

  1. 强转换为字符串类型时优先调用toString方法,强转换为数字类型优先调用valueOf;
  2. 在有运算操作符的情况下,toString和valueOf的优先级是:valueOf > toString;
  3. 进行对象转换时,toString和valueOf的优先级是:toString > valueOf,toString方法如果没有重写的话,会去看valueOf方法是否重写,如果两个方法都没有重写,那么就按照对象的toString方法输出。
    在看了看了上面的总结之后,我们来分析一下如何写这个代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * x == 1 && x == 2 && x == 3想要成立的话,x则应该是对象
    * 当x为对象时,我们可以改写对象下的toString或者valueOf方法
    * /

    //改写toString方法
    const x = {
    val: 0,
    toString: () => ++x.val,
    }

    //改写valueOf方法
    const x = {
    val: 0,
    valueOf: () => ++x.val,
    }
    所以当执行x == 1 && x == 2 && x == 3时,等式比较会触发三次,使得val值递增三次,分别为1、2、3,可以看到如下图所示:
    结果

4. 延展

在介绍完上面的写法之后,我们再来看看对象转基本类型的实践与解析:
注意:
在实践之前需要记住的规则:只有当加法运算时,其中一方是字符串类型,就会把另一方也转化为字符串类型;而其他运算只要其中一方是数字,那么另一方就会转化为数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 例一
1 + {} // '1[object Object]'
/**
* ({}).valueOf() ---> {}
* ({}).toString() ---> '[object Object]'
* 解析:
*(1)由于上面说的规则,先看见数字1,此时右边的对象{}就会被转化成一个数字;
*(2)由于转化类型是数字类型,它首先会调用valueOf()方法,此时还是返回对象{},
* 接着就会继续调用toString()方法,此时会被转化成'[object Object]'基本字符串类型;
*(3)数字1再和字符串相加肯定就得到了上面我们所看到的结果'1[object Object]'。
*/

// 例二
"1" + {} // "1[object Object]"
/**
* 解析:
* 虽然和例一中得到的结果是一致的,但是其解析过程是不一样的,对于例二的解析步骤:
*(1)先看见字符串1,此时右边的对象{}就会被转换成一个字符串;
*(2)对象{}被转换成一个字符串,首先会调用toString()方法将
* 其转化成'[object Object]'基本数字类型;
*(3)最后字符串"1"再和"[object Object]"字符串进行拼接得到结果"1[object Object]"。
*/

// ----------------------难度升级-----------------------
// 例三
[] + [] // ""
/**
* 解析:
* 两边都是非数字或非字符串,记住一个原则:按照valueOf() ---> toString()顺序
* [].valueOf() ----> []
* [].toString() ----> ""
*(1)按照说的规则,左右两边的数组[]都会去转化为数字类型,
* 也就是valueOf--->toString顺序来执行;
*(2)数组[]转化为数字类型,首先会调用valueOf()方法,此时会返回数组[],
* 由于还是对象类型,它会接着去调用toString()方法,然后返回一个空的字符串"";
*(3)所以[]+[]就相当于""+""然后得到""空字符。
*/

// 例四
[] + {} // "[object Object]"
/**
* 解析:
*(1)对于两边都是非数字或者非字符串,那么数组[]和对象{}都会被转换成数字类型;
*(2)[]首先会去调用valueOf()方法转化为数组[],由于它是对象类型又会接着调用
* toString()方法,然后返回一个空的字符串"";
*(3){}首先会去调用valueOf()方法转化为{},由于它是对象类型又会接着调用
* toString()方法,然后返回字符串"[object Object]";
*(4)""+"[object Object]"得到最终结果字符串"[object Object]"。
*/

// --------------难度再次升级-----------------------
// 例五
{} + [] // 0
/**
* 解析:
* {}可以看成是对象,但是大家有没有想过它也可以是代码块呢。如果{}放到前面的话,
* 浏览器会把它解析成代码块,代码块意味着它是什么用都没有的。这个时候后面的"+"
* 并不是二元运算符号,而是一个一元运算符。这个时候数组[]变为数字类型,
* 经过valueOf()和toString()方法之后变为空字符串,然后+""一元符将空字符串转化为数字0。
*/

// 例六
// 它是一个表达式
({} + []) // "[object Object]"
/**
* 解析:
* 例六就是对例五的一个扩展,其实再外层添加括号才能得到我们最开始想要的结果值。
* 这个时候在外层添加括号之后{}就不会被解析为代码块,会被解析为一个对象,至于过程中的
* 对象{}和数组[]都会按照valueOf()和toString()的顺序来解析,最终得到"[object Object]"返回值。
*/

// 例七
{} + {} // NaN
/**
* 解析:
* 首先需要声明:火狐浏览器解析出来时NaN,而Chrome谷歌浏览器解析出来是
* "[object Object][object Object]",此时需要看你的浏览器怎么解析了
*
* 按照火狐浏览器来解析:第一个{}会被解析为代码块,然后将其忽略掉,
* 然后加号会被解析为一元运算符,第二个{}对象会被解析为数字,首先执行valueOf()返回{}对象
* ,然后调用toString()返回字符串"[object Object]",由于+"[object Object]"会被转化
* 为数字,相当于隐私调用Number("[object Object]"),最后给你返回NaN的值
*
* Chrome解析,和例五有区别,总结:
* 如果第一个(前面)是{}时,后面加上其他的像数组、数字或字符串,这时候加号运算
* 会直接变为一元运算符,也就是强制转为数字的运算。
* ({}).valueOf() -> {}
* ({}).toString() -> '[object Object]'
*/

通过上述代码,如果我们期待第一个{}不是一个代码块而是一个对象时,可以有两种做法:
(1)可以考虑强制把输入解析成一个表达式来修复,比如:{}+{}转换成({}+{});
(2)也可以考虑函数或方法的参数也会被解析成一个表达式,比如console.log({}+{}),它得到的结果也是"[object Object][object Object]"

特别感谢下面文章的作者:
如何让 x == 1 && x == 2 && x == 3 等式成立

JS的{} + {}与{} + []的结果是什么?