从一张表情包浅析JS的特性

JS 中 奇怪的行为

Posted by MurphyChen on March 24, 2022

正式阅读本文之前,先来做做下面这个 JS 测试吧!第一次做的时候你可能不及格,但看完本文后相信你可以拿到很高的分数。

JS Is Weird

主要是解析,不会讲太多基础知识,不熟悉隐式转换的同学建议先阅读这篇文章:

https://juejin.cn/post/6844903694039777288

先来道难题,解释以下结果返回值为何为 '10' ?看完全文,相信你能够解答。

1
++[[]][+[]]+[+[]]

只要是前端方向的同学,应该看过这张 JS 作者的表情包:

JS.png

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

不会很详细的讲解的知识点,而是设计到哪个说哪个。这其中大部分是 JS 的隐式转换问题。不了解这部分的同学,建议栈内搜索:JS 隐式转换。

好了,我们开始。

  1. typeof NaN
    这个表达式返回结果为 number,其实就是 ECMAScript 的规定,也符合 IEEE 754 浮点运算标准的规定:(规定嘛,没啥好说的)
    1
    2
    
    # 4.4.27 NaN
    Number value that is an “Not-a-Number” value
    

    来自 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 显然已经超过了这个范围。 超过数字表示范围的计算来谈计算的都是流氓!
    1
    2
    
    // JS 最大安全整数
    Number.MAX_SAFE_INTEGER === 2 ** 53 - 1
    
  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 规则一步一步分析。
    取反操作符 ! 和弹目运算符 + 优先级高,先运算。结果:
    1
    2
    3
    4
    5
    
    // +[] 为 0,![] 为 false
    原式 => !0 + [] + false
        => true + [] + false
        => true + '' + false
        => 'truefalse'
    

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

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

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

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

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

欢迎纠错!!