Skip to content

Thanks for inventing JavaScript

先来张祖师爷的图镇站:

Thanks for inventing JavaScript

接下来,我将一一分析这其中的原理。

不会很详细的讲解的知识点,这其中大部分是 JS 的隐式转换问题。不了解这部分的同学,建议提前搜索相关知识。

好了,我们开始。

1. typeof NaN

这个表达式返回结果为 number,其实就是 ECMAScript 的规定,也符合 IEEE 754 浮点运算标准的规定:(规定嘛,没啥好说的)

# 4.4.27 NaN
Number value that is an “Not-a-Number” value
1
2

来自 ECMA-262 文档:https://tc39.es/ecma262/#sec-terms-and-definitions-nan

2. 9999999999999999

几个 9?不用数了,我替你们数好了,一共 16 位十进制,为什么 16 位的 9999999999999999 返回的结果是 17 位的 10000000000000000 呢?这就涉及到 JS 的最大整数表示范围了,也就是再这个范围内进行运算可以保证精确。根据 IEEE754 标准,JS 的最大安全整数是 2^53 - 1,也就是 Number.MAX_SAFE_INTEGER2^53-1 计算结果为 9007199254740991,一共是 16 位十进制,而 16 位的 9...9 显然已经超过了这个范围。 超过数字表示范围的计算来谈计算的都是流氓!

js
// JS 最大安全整数
Number.MAX_SAFE_INTEGER === 2 ** 53 - 1
1
2

3. 0.1 + 0.2 == 0.3

返回结果为 false,这个是比较经典的浮点数精度问题。很多文章都讲了这个,也是面试常考,我就简单说一下。
计算机内部以二进制存储数字,JS 在计算数字加法的时候,会先转换成二进制再进行计算,而,0.10.2 的二进制相当于一个无限循环小数,两者相加后,计算机只能在精确到某一位,然后舍弃后面的位数,然后这两个数的二进制在相加后又要进行舍弃,就不等于 0.3 了,而是 0.30000000000000004

4. Math.max()

对于外行人,肯定一脸蒙,最大值居然返回无穷小?
但对于学 JS 的人,我们都知道 Math.max() 接收一个数组,然后返回这个数组中最大的数值。那么当参数为空的时候为什么返回 -Infinity 呢?看起来很疑惑,求最大值却返回一个无穷小。
这也是 JS 底层设计的,就是说编写这门语言的时候就是这么规定的。其实仔细想想也挺合理。例如我们要求一个最大值,我们一般设置一个变量 _max,遍历待求的数字序列中,如果有比 _max 大的就替换掉 _max。那么如何保证后面的数字第一次和 _max 比较的时候能比 _max 大呢?这时候就想到了如果 _max 是一个无穷大的值,那么它未来必定会被替换掉。(有点强行解释的味道 hhh)
至于 Math.min() 返回 Infinity,理由同上。

5. [] + []

好家伙,两个空数组相加,返回一个空字符串 ""。 且听我慢慢分析。

在 JS 中,两个对象相加,会触发 隐式转换

这个隐式转换肯定要有规则,对象的隐式转换规则就是 ToPrimitive 规则。这里不展开说 ToPrimitive 规则了,不了解的自行站内搜索。现在你只需要知道,两个对象在进行 + 相加(可理解为拼接)运算的时候会根根据这个规则进行隐式转换。

ToPrimitive 规则:则对象强制转为原始类型,首先查找对象自身是否有 valueOf 方法,若有则执行该方法;若没有则执行 toString 方法(有些情况例如 Date 对象先执行 toString)。

对于空数组 [],执行 [].valueof(),返回结果依然为 [],不是一个原始类型,则执行 [].toString() 方法,JS 底层会根据 ToString 规则(注意不是 toString,而是其他类型转字符串类型的规则),结果会返回 ""。那么结果很明显了,两个空字符串相加,结果自然为 ""

后面很多分析都要用到隐式转换及 ToPrimitive 转换规则,不了解的可以自行搜索。

6. [] + {}

计算结果为 "[object Object]"。分析步骤与上一个类似:两对象相加,触发 ToPrimitive 转换,首先执行 valueOf() 方法(如果有),空对象的 valueOf() 依然返回 {},则执行 toString(),根据 ToString 规则,一个普通对象转换为一个字符串的结果为 "[object Object]"
经上述分析,[] 隐式转换为 ""{} 转换为 "[object Object]",字符串一拼接,结果即为 "[object Object]"

7. {} + []

这个和前面就换了个位置,结果就变为 0 了?此时你肯定要非常想吐槽 JS 是什么垃圾语言。其实这个是 JS 解析策略的问题,对于一行代码开头的 {} ,JS 可以解析为一个空对象,也可以解析为一块 代码块,这块代码块为空,什么都不做。于是表达式就可以简化为 +[],关键来了,+ 这个操作符在做二元操作符时,表示相加或者拼接,在做单目运算符的时候表示正的,例如 +0,也可以把一个非整数类型转换为一个整数,相当于 Number() 方法。单目运算符 + 会触发 ToNumber 隐式转换
现在来看 +[]

ToNumber 规则规定,对象或者数组转为数字类型,首先要经过 ToPrimitive规则转换。

对于空数组 [] 的 ToPrimitive 转换,我们已经分析过了,结果为空串 ""。那么空串的 ToNumber 转换(也就是 Number(""))结果是啥呢?根据 ToNumber 规则,结果为 0,也即最终结果。

8. true + true + true

结果为 3。我们现在逐渐熟悉这种分析过程了。非数值的原始类型进行相加,JS 也设定了相应的转换规则(这里主要讲数字、布尔、字符串、null、undefined 这五类。):

  1. 布尔、数字、null、undfined 类型之间相加,全部转换为数字类型再相加(Number(undefined)NaN
  2. 字符串和其他类型相加,全部转换为字符串,再拼接 true+true+true 满足第一条,全部转换为数字类型:1+1+1===3
    其实类似的还有 null+true===1null+'123'===null123,都可以使用上述规则解。

9. true - true

结果为 0。这里涉及到减法了。JS 对于非数值的原始类型(主要讲数字、布尔、字符串、null、undefined)之间的减法也有特殊规则:

对于非数值的原始类型之间的减法,只需要记住一点:
先全部转换为数值类型,然后再做减法。例如 '100'-true === 100 - 1 === 99
注意 Number(null)===0Number(undefine)NaNtrue-true => 全部转换为数值:1 - 1 === 0

10. true == 1

结果为 true。同样的,在等于比较 == 时候,会触发隐式转换,等于号隐式转换规则:

  1. 布尔类型和其他类型的相等比较,布尔先转为数字类型
  2. 数字类型和字符串类型的相等比较,字符串类型先转数字类型
  3. 对象类型和原始类型的相等比较,对象类型会依照 ToPrimitive 规则转换为原始类型
  4. nullundfined 和其他值比较
    • nullundfined 以及自身相等 ==,和其他值不相等。(ECMAScript 规定不再触发转换)
    • undfinednull 以及自身相等 ==,和其他值不相等。

true==1 => 1==1 => true

11. true === 1

结果为 false。全等号 === 没有隐式转换,直接比较类型和值。

12. (!+[]+[]+![]).length

结果为 9。咱们按照 ToPrimitive 规则一步一步分析。
取反操作符 ! 和弹目运算符 + 优先级高,先运算。结果:

// +[] 为 0,![] 为 false
原式 => !0 + [] + false
    => true + [] + false
    => true + '' + false
    => 'truefalse'
1
2
3
4
5

最后,结果为一个字符串 'truefalse',长度为 9

13. 对于 9+"1""91"-1,看上述第 8、9 条规则。

14. [] == 0

结果为 true。这种情况属于对象和原始类型之间的 == 比较。 [] 进行 ToPrimitive 隐式转换,结果为 ""
现在进行原始类型之间的比较:""==0
根据规则:数字与字符串进行比较时,会尝试将字符串转换为数字值。
""==0 => 0==0 => true

宽松等于比较 ==规则:MDN ==

看吧,看起来不合理的东西,只是你不了解它罢了。

欢迎纠错!!