Appearance
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
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_INTEGER
。2^53-1
计算结果为 9007199254740991
,一共是 16
位十进制,而 16 位的 9...9
显然已经超过了这个范围。 超过数字表示范围的计算来谈计算的都是流氓!
js
// JS 最大安全整数
Number.MAX_SAFE_INTEGER === 2 ** 53 - 1
1
2
2
3. 0.1 + 0.2 == 0.3
返回结果为 false
,这个是比较经典的浮点数精度问题。很多文章都讲了这个,也是面试常考,我就简单说一下。
计算机内部以二进制存储数字,JS 在计算数字加法的时候,会先转换成二进制再进行计算,而,0.1
和 0.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 这五类。):
- 布尔、数字、null、undfined 类型之间相加,全部转换为数字类型再相加(
Number(undefined)
为NaN
)- 字符串和其他类型相加,全部转换为字符串,再拼接
true+true+true
满足第一条,全部转换为数字类型:1+1+1===3
。
其实类似的还有null+true===1
、null+'123'===null123
,都可以使用上述规则解。
9. true - true
结果为 0
。这里涉及到减法了。JS 对于非数值的原始类型(主要讲数字、布尔、字符串、null、undefined)之间的减法也有特殊规则:
对于非数值的原始类型之间的减法,只需要记住一点:
先全部转换为数值类型,然后再做减法。例如'100'-true === 100 - 1 === 99
注意Number(null)===0
,Number(undefine)
为NaN
。true-true
=> 全部转换为数值:1 - 1 === 0
10. true == 1
结果为 true
。同样的,在等于比较 ==
时候,会触发隐式转换,等于号隐式转换规则:
- 布尔类型和其他类型的相等比较,布尔先转为数字类型
- 数字类型和字符串类型的相等比较,字符串类型先转数字类型
- 对象类型和原始类型的相等比较,对象类型会依照
ToPrimitive
规则转换为原始类型null
、undfined
和其他值比较
null
和undfined
以及自身相等==
,和其他值不相等。(ECMAScript 规定不再触发转换)undfined
和null
以及自身相等==
,和其他值不相等。
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
2
3
4
5
最后,结果为一个字符串 'truefalse'
,长度为 9
。
13. 对于 9+"1"
和 "91"-1
,看上述第 8、9 条规则。
14. [] == 0
结果为 true
。这种情况属于对象和原始类型之间的 ==
比较。 []
进行 ToPrimitive 隐式转换,结果为 ""
现在进行原始类型之间的比较:""==0
。
根据规则:数字与字符串进行比较时,会尝试将字符串转换为数字值。""==0
=> 0==0
=> true
宽松等于比较
==
规则:MDN ==
看吧,看起来不合理的东西,只是你不了解它罢了。
欢迎纠错!!